feat: add Immich API client and photo proxy routes
Implements ImmichClient with list_albums, get_album, get_thumbnail, get_original methods; wraps connection errors as ConnectionError. Adds /proxy/thumb/<asset_id> and /proxy/original/<asset_id> Flask routes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class ImmichClient:
|
||||||
|
def __init__(self, base_url: str, api_key: str):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
|
|
||||||
|
def _get(self, path: str, **kwargs):
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{self.base_url}{path}",
|
||||||
|
headers=self.headers, timeout=10, **kwargs)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
raise ConnectionError(f"Cannot reach Immich: {e}") from e
|
||||||
|
|
||||||
|
def list_albums(self) -> list:
|
||||||
|
return self._get("/api/albums").json()
|
||||||
|
|
||||||
|
def get_album(self, album_id: str) -> dict:
|
||||||
|
return self._get(f"/api/albums/{album_id}",
|
||||||
|
params={"withoutAssets": "false"}).json()
|
||||||
|
|
||||||
|
def get_thumbnail(self, asset_id: str) -> bytes:
|
||||||
|
return self._get(f"/api/assets/{asset_id}/thumbnail",
|
||||||
|
params={"size": "preview"}).content
|
||||||
|
|
||||||
|
def get_original(self, asset_id: str) -> bytes:
|
||||||
|
return self._get(f"/api/assets/{asset_id}/original").content
|
||||||
@@ -1,3 +1,29 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint, current_app, Response, abort
|
||||||
|
from app.immich import ImmichClient
|
||||||
|
|
||||||
bp = Blueprint("proxy", __name__)
|
bp = Blueprint("proxy", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _client() -> ImmichClient:
|
||||||
|
return ImmichClient(
|
||||||
|
base_url=current_app.config["IMMICH_URL"],
|
||||||
|
api_key=current_app.config["IMMICH_API_KEY"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/proxy/thumb/<asset_id>")
|
||||||
|
def thumb(asset_id):
|
||||||
|
try:
|
||||||
|
data = _client().get_thumbnail(asset_id)
|
||||||
|
except ConnectionError:
|
||||||
|
abort(502)
|
||||||
|
return Response(data, content_type="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/proxy/original/<asset_id>")
|
||||||
|
def original(asset_id):
|
||||||
|
try:
|
||||||
|
data = _client().get_original(asset_id)
|
||||||
|
except ConnectionError:
|
||||||
|
abort(502)
|
||||||
|
return Response(data, content_type="image/jpeg")
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import pytest
|
||||||
|
from app.immich import ImmichClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(mock_immich):
|
||||||
|
return ImmichClient(
|
||||||
|
base_url=f"http://127.0.0.1:8099",
|
||||||
|
api_key="test-key",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_albums(client):
|
||||||
|
albums = client.list_albums()
|
||||||
|
assert len(albums) == 1
|
||||||
|
assert albums[0]["albumName"] == "Central Asia 2023"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_album(client):
|
||||||
|
album = client.get_album("album-1")
|
||||||
|
assert len(album["assets"]) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_thumbnail_returns_bytes(client):
|
||||||
|
data = client.get_thumbnail("asset-1")
|
||||||
|
assert isinstance(data, bytes)
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_original_returns_bytes(client):
|
||||||
|
data = client.get_original("asset-1")
|
||||||
|
assert isinstance(data, bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_albums_connection_error_raises(monkeypatch):
|
||||||
|
client = ImmichClient(base_url="http://127.0.0.1:1", api_key="x")
|
||||||
|
with pytest.raises(ConnectionError):
|
||||||
|
client.list_albums()
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_thumb_route(base_url, page, seed_state):
|
||||||
|
seed_state("phase2_state")
|
||||||
|
page.goto(f"{base_url}/proxy/thumb/asset-1")
|
||||||
|
assert page.evaluate("document.contentType").startswith("image/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_original_route(base_url, page, seed_state):
|
||||||
|
seed_state("phase2_state")
|
||||||
|
page.goto(f"{base_url}/proxy/original/asset-1")
|
||||||
|
assert page.evaluate("document.contentType").startswith("image/")
|
||||||
Reference in New Issue
Block a user