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.
220 lines
5.4 KiB
Go
220 lines
5.4 KiB
Go
package photos
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewClient(baseURL string) *Client {
|
|
return &Client{
|
|
baseURL: baseURL,
|
|
httpClient: &http.Client{
|
|
Timeout: 60 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
type Asset struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
OriginalPath string `json:"originalPath"`
|
|
OriginalName string `json:"originalFileName"`
|
|
MimeType string `json:"originalMimeType"`
|
|
FileSize int64 `json:"fileSizeInByte"`
|
|
CreatedAt string `json:"fileCreatedAt"`
|
|
IsFavorite bool `json:"isFavorite"`
|
|
ThumbHash string `json:"thumbhash"`
|
|
ExifInfo struct {
|
|
FileSizeInByte int64 `json:"fileSizeInByte"`
|
|
} `json:"exifInfo,omitempty"`
|
|
}
|
|
|
|
type Album struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"albumName"`
|
|
Description string `json:"description,omitempty"`
|
|
AssetCount int `json:"assetCount"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt,omitempty"`
|
|
}
|
|
|
|
const assetsPageSize = 200
|
|
|
|
func (c *Client) GetAllAssets(ctx context.Context, apiKey string) ([]Asset, error) {
|
|
var all []Asset
|
|
page := 1
|
|
for {
|
|
batch, err := c.GetAssets(ctx, apiKey, page, assetsPageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
all = append(all, batch...)
|
|
if len(batch) < assetsPageSize {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
return all, nil
|
|
}
|
|
|
|
func (c *Client) GetAssets(ctx context.Context, apiKey string, page, size int) ([]Asset, error) {
|
|
url := fmt.Sprintf("%s/assets?page=%d&size=%d", c.baseURL, page, size)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("x-api-key", apiKey)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
io.Copy(io.Discard, resp.Body)
|
|
return nil, &HTTPStatusError{Operation: "get assets", StatusCode: resp.StatusCode}
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var assets []Asset
|
|
trimmed := bytes.TrimSpace(body)
|
|
switch {
|
|
case len(trimmed) == 0:
|
|
return []Asset{}, nil
|
|
case trimmed[0] == '[':
|
|
if err := json.Unmarshal(trimmed, &assets); err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
var wrapped struct {
|
|
Items []Asset `json:"items"`
|
|
Assets []Asset `json:"assets"`
|
|
}
|
|
if err := json.Unmarshal(trimmed, &wrapped); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(wrapped.Items) > 0 {
|
|
assets = wrapped.Items
|
|
} else {
|
|
assets = wrapped.Assets
|
|
}
|
|
}
|
|
for i := range assets {
|
|
if assets[i].FileSize == 0 {
|
|
assets[i].FileSize = assets[i].ExifInfo.FileSizeInByte
|
|
}
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func (c *Client) GetAlbums(ctx context.Context, apiKey string) ([]Album, error) {
|
|
url := fmt.Sprintf("%s/albums", c.baseURL)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("x-api-key", apiKey)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
io.Copy(io.Discard, resp.Body)
|
|
return nil, &HTTPStatusError{Operation: "get albums", StatusCode: resp.StatusCode}
|
|
}
|
|
|
|
var albums []Album
|
|
if err := json.NewDecoder(resp.Body).Decode(&albums); err != nil {
|
|
return nil, err
|
|
}
|
|
return albums, nil
|
|
}
|
|
|
|
func (c *Client) UploadAsset(ctx context.Context, apiKey string, body io.Reader, contentType string) (string, error) {
|
|
url := fmt.Sprintf("%s/assets", c.baseURL)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("x-api-key", apiKey)
|
|
req.Header.Set("Content-Type", contentType)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
io.Copy(io.Discard, resp.Body)
|
|
return "", &HTTPStatusError{Operation: "upload asset", StatusCode: resp.StatusCode}
|
|
}
|
|
|
|
var result struct {
|
|
ID string `json:"id"`
|
|
AssetID string `json:"assetId"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return "", err
|
|
}
|
|
if result.ID != "" {
|
|
return result.ID, nil
|
|
}
|
|
return result.AssetID, nil
|
|
}
|
|
|
|
func (c *Client) GetAssetThumbnail(ctx context.Context, apiKey, assetID string) (io.ReadCloser, string, error) {
|
|
url := fmt.Sprintf("%s/assets/%s/thumbnail", c.baseURL, assetID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
req.Header.Set("x-api-key", apiKey)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
return nil, "", &HTTPStatusError{Operation: "get asset thumbnail", StatusCode: resp.StatusCode}
|
|
}
|
|
|
|
return resp.Body, resp.Header.Get("Content-Type"), nil
|
|
}
|
|
|
|
func (c *Client) DeleteAsset(ctx context.Context, apiKey, assetID string) error {
|
|
url := fmt.Sprintf("%s/assets/%s", c.baseURL, assetID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("x-api-key", apiKey)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
io.Copy(io.Discard, resp.Body)
|
|
return &HTTPStatusError{Operation: "delete asset", StatusCode: resp.StatusCode}
|
|
}
|
|
return nil
|
|
}
|