ultisuite-backend/internal/photos/albums.go
R3D347HR4Y f0f0b31043 Implement Photos API robustness and quota integration
Improve Immich-backed photos endpoints with robust mapping/error handling, full albums CRUD, reliable list pagination/sorting/filtering, and shared Nextcloud quota checks before upload.
2026-05-22 21:09:13 +02:00

130 lines
3.7 KiB
Go

package photos
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
type BulkAssetResult struct {
ID string `json:"id"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
type createAlbumPayload struct {
AlbumName string `json:"albumName"`
Description string `json:"description,omitempty"`
AssetIDs []string `json:"assetIds,omitempty"`
}
type updateAlbumPayload struct {
AlbumName *string `json:"albumName,omitempty"`
Description *string `json:"description,omitempty"`
}
type bulkIDsPayload struct {
IDs []string `json:"ids"`
}
func (c *Client) GetAlbum(ctx context.Context, apiKey, albumID string) (Album, error) {
url := fmt.Sprintf("%s/albums/%s?withoutAssets=true", c.baseURL, albumID)
var album Album
if err := c.doJSON(ctx, http.MethodGet, url, apiKey, nil, &album, "get album", http.StatusOK); err != nil {
return Album{}, err
}
return album, nil
}
func (c *Client) CreateAlbum(ctx context.Context, apiKey, name, description string, assetIDs []string) (Album, error) {
payload := createAlbumPayload{
AlbumName: name,
Description: description,
AssetIDs: assetIDs,
}
var album Album
if err := c.doJSON(ctx, http.MethodPost, c.baseURL+"/albums", apiKey, payload, &album, "create album", http.StatusCreated); err != nil {
return Album{}, err
}
return album, nil
}
func (c *Client) UpdateAlbum(ctx context.Context, apiKey, albumID string, name, description *string) (Album, error) {
payload := updateAlbumPayload{
AlbumName: name,
Description: description,
}
url := fmt.Sprintf("%s/albums/%s", c.baseURL, albumID)
var album Album
if err := c.doJSON(ctx, http.MethodPatch, url, apiKey, payload, &album, "update album", http.StatusOK); err != nil {
return Album{}, err
}
return album, nil
}
func (c *Client) DeleteAlbum(ctx context.Context, apiKey, albumID string) error {
url := fmt.Sprintf("%s/albums/%s", c.baseURL, albumID)
return c.doJSON(ctx, http.MethodDelete, url, apiKey, nil, nil, "delete album", http.StatusOK, http.StatusNoContent)
}
func (c *Client) AddAlbumAssets(ctx context.Context, apiKey, albumID string, assetIDs []string) ([]BulkAssetResult, error) {
url := fmt.Sprintf("%s/albums/%s/assets", c.baseURL, albumID)
var results []BulkAssetResult
if err := c.doJSON(ctx, http.MethodPut, url, apiKey, bulkIDsPayload{IDs: assetIDs}, &results, "add album assets", http.StatusOK); err != nil {
return nil, err
}
return results, nil
}
func (c *Client) RemoveAlbumAssets(ctx context.Context, apiKey, albumID string, assetIDs []string) ([]BulkAssetResult, error) {
url := fmt.Sprintf("%s/albums/%s/assets", c.baseURL, albumID)
var results []BulkAssetResult
if err := c.doJSON(ctx, http.MethodDelete, url, apiKey, bulkIDsPayload{IDs: assetIDs}, &results, "remove album assets", http.StatusOK); err != nil {
return nil, err
}
return results, nil
}
func (c *Client) doJSON(ctx context.Context, method, url, apiKey string, reqBody any, dest any, operation string, okStatuses ...int) error {
var body io.Reader
if reqBody != nil {
data, err := json.Marshal(reqBody)
if err != nil {
return err
}
body = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return err
}
req.Header.Set("x-api-key", apiKey)
if reqBody != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
for _, status := range okStatuses {
if resp.StatusCode == status {
if dest != nil {
if err := json.NewDecoder(resp.Body).Decode(dest); err != nil {
return err
}
}
return nil
}
}
io.Copy(io.Discard, resp.Body)
return &HTTPStatusError{Operation: operation, StatusCode: resp.StatusCode}
}