feat: person excludes for auto-organize rules, backup & restore system

Add person exclude criteria to Immich auto-organize — assets containing
excluded persons are filtered out after candidate gathering. Also adds
full backup/restore system with export, import, scheduled backups, and
retention management.
This commit is contained in:
2026-04-02 14:13:42 +03:00
parent 6e51164f8e
commit 6b2211353d
13 changed files with 2191 additions and 2 deletions
@@ -0,0 +1,269 @@
"""Pydantic models for the configuration backup/restore file format."""
from __future__ import annotations
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
class SecretsMode(str, Enum):
EXCLUDE = "exclude"
MASKED = "masked"
INCLUDE = "include"
class ConflictMode(str, Enum):
SKIP = "skip"
RENAME = "rename"
OVERWRITE = "overwrite"
class BackupCategory(str, Enum):
PROVIDERS = "providers"
TELEGRAM_BOTS = "telegram_bots"
MATRIX_BOTS = "matrix_bots"
EMAIL_BOTS = "email_bots"
TARGETS = "targets"
TRACKING_CONFIGS = "tracking_configs"
TEMPLATE_CONFIGS = "template_configs"
COMMAND_CONFIGS = "command_configs"
COMMAND_TEMPLATE_CONFIGS = "command_template_configs"
NOTIFICATION_TRACKERS = "notification_trackers"
COMMAND_TRACKERS = "command_trackers"
ACTIONS = "actions"
APP_SETTINGS = "app_settings"
ALL_CATEGORIES = list(BackupCategory)
# Secret fields in provider config dicts
PROVIDER_SECRET_FIELDS = frozenset(
("api_key", "api_token", "webhook_secret", "password",
"client_secret", "refresh_token")
)
# ---------- nested child models ----------
class ReceiverData(BaseModel):
name: str = ""
config: dict[str, Any] = {}
receiver_key: str = ""
locale: str = ""
enabled: bool = True
class TargetData(BaseModel):
id: int
type: str
name: str
icon: str = ""
config: dict[str, Any] = {}
chat_action: str | None = "typing"
receivers: list[ReceiverData] = []
class TemplateSlotData(BaseModel):
slot_name: str
locale: str = "en"
template: str = ""
class TemplateConfigData(BaseModel):
id: int
provider_type: str
name: str
description: str = ""
icon: str = ""
locale: str = ""
date_format: str = "%d.%m.%Y, %H:%M UTC"
date_only_format: str = "%d.%m.%Y"
slots: list[TemplateSlotData] = []
class CommandTemplateSlotData(BaseModel):
slot_name: str
locale: str = "en"
template: str = ""
class CommandTemplateConfigData(BaseModel):
id: int
provider_type: str
name: str
description: str = ""
icon: str = ""
locale: str = ""
slots: list[CommandTemplateSlotData] = []
class TrackerTargetData(BaseModel):
target_id: int
tracking_config_id: int | None = None
template_config_id: int | None = None
enabled: bool = True
quiet_hours_start: str | None = None
quiet_hours_end: str | None = None
class NotificationTrackerData(BaseModel):
id: int
provider_id: int
name: str
icon: str = ""
collection_ids: list[str] = []
filters: dict[str, Any] = {}
scan_interval: int = 60
batch_duration: int = 0
default_tracking_config_id: int | None = None
default_template_config_id: int | None = None
enabled: bool = True
targets: list[TrackerTargetData] = []
class CommandTrackerListenerData(BaseModel):
listener_type: str
listener_id: int
class CommandTrackerData(BaseModel):
id: int
provider_id: int
command_config_id: int
name: str
icon: str = ""
enabled: bool = True
listeners: list[CommandTrackerListenerData] = []
class ActionRuleData(BaseModel):
name: str = ""
rule_config: dict[str, Any] = {}
enabled: bool = True
order: int = 0
class ActionData(BaseModel):
id: int
provider_id: int
name: str
icon: str = ""
action_type: str
config: dict[str, Any] = {}
schedule_type: str = "interval"
schedule_interval: int = 3600
schedule_cron: str = ""
enabled: bool = False
rules: list[ActionRuleData] = []
class ProviderData(BaseModel):
id: int
type: str
name: str
icon: str = ""
config: dict[str, Any] = {}
class TelegramBotData(BaseModel):
id: int
name: str
token: str = ""
icon: str = ""
bot_username: str = ""
update_mode: str = "polling"
class MatrixBotData(BaseModel):
id: int
name: str
icon: str = ""
homeserver_url: str = ""
access_token: str = ""
display_name: str = ""
class EmailBotData(BaseModel):
id: int
name: str
icon: str = ""
email: str = ""
smtp_host: str = ""
smtp_port: int = 587
smtp_username: str = ""
smtp_password: str = ""
smtp_use_tls: bool = True
class TrackingConfigData(BaseModel):
id: int
provider_type: str
name: str
icon: str = ""
# All the boolean / int / str tracking fields are captured generically
fields: dict[str, Any] = {}
class CommandConfigData(BaseModel):
id: int
provider_type: str
name: str
icon: str = ""
enabled_commands: list[str] = []
response_mode: str = "media"
default_count: int = 5
rate_limits: dict[str, Any] = {}
command_template_config_id: int | None = None
class AppSettingData(BaseModel):
key: str
value: str = ""
# ---------- top-level backup envelope ----------
class BackupData(BaseModel):
providers: list[ProviderData] = []
telegram_bots: list[TelegramBotData] = []
matrix_bots: list[MatrixBotData] = []
email_bots: list[EmailBotData] = []
targets: list[TargetData] = []
tracking_configs: list[TrackingConfigData] = []
template_configs: list[TemplateConfigData] = []
command_configs: list[CommandConfigData] = []
command_template_configs: list[CommandTemplateConfigData] = []
notification_trackers: list[NotificationTrackerData] = []
command_trackers: list[CommandTrackerData] = []
actions: list[ActionData] = []
app_settings: list[AppSettingData] = []
class BackupFile(BaseModel):
format: str = "notify-bridge-backup"
version: int = 1
created_at: str = ""
app_version: str = ""
secrets_mode: SecretsMode = SecretsMode.EXCLUDE
categories: list[str] = []
data: BackupData = Field(default_factory=BackupData)
# ---------- import result ----------
class ImportResult(BaseModel):
created: int = 0
skipped: int = 0
overwritten: int = 0
errors: list[str] = []
warnings: list[str] = []
class ValidateResult(BaseModel):
valid: bool = True
version: int = 0
entity_counts: dict[str, int] = {}
warnings: list[str] = []
errors: list[str] = []