Some checks failed
Validate / Hassfest (push) Has been cancelled
Backend: Fix CORS wildcard+credentials, add secret key warning, remove raw API keys from sync endpoint, fix N+1 queries in watcher/sync, fix AttributeError on event_types, delete dead scheduled.py/templates.py, add limit cap on history, re-validate server on URL/key update, apply tracking/template config IDs in update_target. HA Integration: Replace datetime.now() with dt_util.now(), fix notification queue to only remove successfully sent items, use album UUID for entity unique IDs, add shared links dirty flag and users cache hourly refresh, deduplicate _is_quiet_hours, add HTTP timeouts, cache albums in config flow, change iot_class to local_polling. Frontend: Make i18n reactive via $state (remove window.location.reload), add Modal transitions/a11y/Escape key, create ConfirmModal replacing all confirm() calls, add error handling to all pages, replace Unicode nav icons with MDI SVGs, add card hover effects, dashboard stat icons, global focus-visible styles, form slide transitions, mobile responsive bottom nav, fix password error color, add ~20 i18n keys (EN/RU). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
"""Immich server management API routes."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlmodel import select
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
import aiohttp
|
|
|
|
from immich_watcher_core.immich_client import ImmichClient
|
|
|
|
from ..auth.dependencies import get_current_user
|
|
from ..database.engine import get_session
|
|
from ..database.models import ImmichServer, User
|
|
|
|
router = APIRouter(prefix="/api/servers", tags=["servers"])
|
|
|
|
|
|
class ServerCreate(BaseModel):
|
|
name: str = "Immich"
|
|
url: str
|
|
api_key: str
|
|
|
|
|
|
class ServerUpdate(BaseModel):
|
|
name: str | None = None
|
|
url: str | None = None
|
|
api_key: str | None = None
|
|
|
|
|
|
class ServerResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
url: str
|
|
created_at: str
|
|
|
|
|
|
@router.get("")
|
|
async def list_servers(
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""List all Immich servers for the current user."""
|
|
result = await session.exec(
|
|
select(ImmichServer).where(ImmichServer.user_id == user.id)
|
|
)
|
|
servers = result.all()
|
|
return [
|
|
{"id": s.id, "name": s.name, "url": s.url, "created_at": s.created_at.isoformat()}
|
|
for s in servers
|
|
]
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
async def create_server(
|
|
body: ServerCreate,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Add a new Immich server (validates connection)."""
|
|
# Validate connection
|
|
async with aiohttp.ClientSession() as http_session:
|
|
client = ImmichClient(http_session, body.url, body.api_key)
|
|
if not await client.ping():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Cannot connect to Immich server at {body.url}",
|
|
)
|
|
# Fetch external domain
|
|
external_domain = await client.get_server_config()
|
|
|
|
server = ImmichServer(
|
|
user_id=user.id,
|
|
name=body.name,
|
|
url=body.url,
|
|
api_key=body.api_key,
|
|
external_domain=external_domain,
|
|
)
|
|
session.add(server)
|
|
await session.commit()
|
|
await session.refresh(server)
|
|
return {"id": server.id, "name": server.name, "url": server.url}
|
|
|
|
|
|
@router.get("/{server_id}")
|
|
async def get_server(
|
|
server_id: int,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Get a specific Immich server."""
|
|
server = await _get_user_server(session, server_id, user.id)
|
|
return {"id": server.id, "name": server.name, "url": server.url, "created_at": server.created_at.isoformat()}
|
|
|
|
|
|
@router.put("/{server_id}")
|
|
async def update_server(
|
|
server_id: int,
|
|
body: ServerUpdate,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Update an Immich server."""
|
|
server = await _get_user_server(session, server_id, user.id)
|
|
if body.name is not None:
|
|
server.name = body.name
|
|
url_changed = body.url is not None and body.url != server.url
|
|
key_changed = body.api_key is not None and body.api_key != server.api_key
|
|
if body.url is not None:
|
|
server.url = body.url
|
|
if body.api_key is not None:
|
|
server.api_key = body.api_key
|
|
# Re-validate and refresh external_domain when URL or API key changes
|
|
if url_changed or key_changed:
|
|
try:
|
|
async with aiohttp.ClientSession() as http_session:
|
|
client = ImmichClient(http_session, server.url, server.api_key)
|
|
if not await client.ping():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Cannot connect to Immich server at {server.url}",
|
|
)
|
|
server.external_domain = await client.get_server_config()
|
|
except aiohttp.ClientError as err:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Connection error: {err}",
|
|
)
|
|
session.add(server)
|
|
await session.commit()
|
|
await session.refresh(server)
|
|
return {"id": server.id, "name": server.name, "url": server.url}
|
|
|
|
|
|
@router.delete("/{server_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_server(
|
|
server_id: int,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Delete an Immich server."""
|
|
server = await _get_user_server(session, server_id, user.id)
|
|
await session.delete(server)
|
|
await session.commit()
|
|
|
|
|
|
@router.get("/{server_id}/ping")
|
|
async def ping_server(
|
|
server_id: int,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Check if an Immich server is reachable."""
|
|
server = await _get_user_server(session, server_id, user.id)
|
|
async with aiohttp.ClientSession() as http_session:
|
|
client = ImmichClient(http_session, server.url, server.api_key)
|
|
ok = await client.ping()
|
|
return {"online": ok}
|
|
|
|
|
|
@router.get("/{server_id}/albums")
|
|
async def list_albums(
|
|
server_id: int,
|
|
user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Fetch albums from an Immich server."""
|
|
server = await _get_user_server(session, server_id, user.id)
|
|
async with aiohttp.ClientSession() as http_session:
|
|
client = ImmichClient(http_session, server.url, server.api_key)
|
|
albums = await client.get_albums()
|
|
return [
|
|
{
|
|
"id": a.get("id"),
|
|
"albumName": a.get("albumName"),
|
|
"assetCount": a.get("assetCount", 0),
|
|
"shared": a.get("shared", False),
|
|
"updatedAt": a.get("updatedAt", ""),
|
|
}
|
|
for a in albums
|
|
]
|
|
|
|
|
|
async def _get_user_server(
|
|
session: AsyncSession, server_id: int, user_id: int
|
|
) -> ImmichServer:
|
|
"""Get a server owned by the user, or raise 404."""
|
|
server = await session.get(ImmichServer, server_id)
|
|
if not server or server.user_id != user_id:
|
|
raise HTTPException(status_code=404, detail="Server not found")
|
|
return server
|