#include "immich_client.h" #include #include #include #include void ImmichClient::begin(const String& baseUrl, const String& apiKey) { _baseUrl = baseUrl; // Remove trailing slash if present if (_baseUrl.endsWith("/")) { _baseUrl.remove(_baseUrl.length() - 1); } _apiKey = apiKey; Serial.printf("[immich] Configured: %s\n", _baseUrl.c_str()); } std::vector ImmichClient::fetchAlbums() { std::vector albums; String url = buildUrl("/api/albums"); String response = httpGet(url); if (response.isEmpty()) { Serial.println("[immich] fetchAlbums: empty response"); return albums; } JsonDocument doc; DeserializationError err = deserializeJson(doc, response); if (err) { Serial.printf("[immich] fetchAlbums JSON error: %s\n", err.c_str()); return albums; } JsonArray arr = doc.as(); for (JsonObject obj : arr) { AlbumInfo album; album.id = obj["id"].as(); album.title = obj["albumName"].as(); album.assetCount = obj["assetCount"] | 0; albums.push_back(album); } Serial.printf("[immich] Fetched %d albums\n", albums.size()); return albums; } std::vector ImmichClient::fetchAlbumAssetIds(const String& albumId) { std::vector ids; String url = buildUrl("/api/albums/" + albumId); String response = httpGet(url); if (response.isEmpty()) return ids; JsonDocument doc; DeserializationError err = deserializeJson(doc, response); if (err) { Serial.printf("[immich] fetchAlbumAssets JSON error: %s\n", err.c_str()); return ids; } JsonArray assets = doc["assets"].as(); for (JsonObject asset : assets) { ids.push_back(asset["id"].as()); } Serial.printf("[immich] Album %s: %d assets\n", albumId.c_str(), ids.size()); return ids; } std::vector ImmichClient::fetchFavoriteAssetIds() { std::vector ids; String url = buildUrl("/api/assets?isFavorite=true"); String response = httpGet(url); if (response.isEmpty()) return ids; JsonDocument doc; DeserializationError err = deserializeJson(doc, response); if (err) return ids; JsonArray arr = doc.as(); for (JsonObject asset : arr) { ids.push_back(asset["id"].as()); } Serial.printf("[immich] Favorites: %d assets\n", ids.size()); return ids; } AssetInfo ImmichClient::fetchAssetInfo(const String& assetId) { AssetInfo info; info.id = assetId; info.isFavorite = false; info.isPortrait = false; String url = buildUrl("/api/assets/" + assetId); String response = httpGet(url); if (response.isEmpty()) return info; JsonDocument doc; DeserializationError err = deserializeJson(doc, response); if (err) return info; info.originalFileName = doc["originalFileName"] | ""; info.isFavorite = doc["isFavorite"] | false; // Date info.dateTime = doc["localDateTime"] | ""; // EXIF data JsonObject exif = doc["exifInfo"]; if (!exif.isNull()) { info.city = exif["city"] | ""; String make = exif["make"] | ""; String model = exif["model"] | ""; if (make.length() > 0 || model.length() > 0) { info.camera = make + " " + model; info.camera.trim(); } // Portrait detection: check orientation or dimensions int width = exif["exifImageWidth"] | 0; int height = exif["exifImageHeight"] | 0; int orientation = exif["orientation"] | 1; // Orientations 5-8 mean the image is rotated 90/270 degrees if (orientation >= 5 && orientation <= 8) { info.isPortrait = (width > height); } else { info.isPortrait = (height > width); } } // People JsonArray people = doc["people"]; if (!people.isNull()) { for (JsonObject person : people) { String name = person["name"] | ""; if (name.length() > 0) { info.people.push_back(name); } } } return info; } bool ImmichClient::downloadAsset(const String& assetId, ImageQuality quality, uint8_t** outBuffer, size_t* outSize) { String url; if (quality == ImageQuality::Original) { url = buildUrl("/api/assets/" + assetId + "/original"); } else { url = buildUrl("/api/assets/" + assetId + "/thumbnail?size=preview"); } return httpGetBinary(url, outBuffer, outSize); } String ImmichClient::buildUrl(const String& path) { return _baseUrl + path; } String ImmichClient::httpGet(const String& url) { WiFiClientSecure client; client.setInsecure(); // Skip TLS cert verification for self-hosted HTTPClient http; http.begin(client, url); http.addHeader("x-api-key", _apiKey); http.setTimeout(30000); int code = http.GET(); String result = ""; if (code == HTTP_CODE_OK) { result = http.getString(); } else { Serial.printf("[immich] HTTP GET %s failed: %d\n", url.c_str(), code); } http.end(); return result; } bool ImmichClient::httpGetBinary(const String& url, uint8_t** outBuffer, size_t* outSize) { WiFiClientSecure client; client.setInsecure(); HTTPClient http; http.begin(client, url); http.addHeader("x-api-key", _apiKey); http.setTimeout(60000); int code = http.GET(); if (code != HTTP_CODE_OK) { Serial.printf("[immich] Binary GET failed: %d\n", code); http.end(); return false; } int contentLength = http.getSize(); if (contentLength <= 0) { Serial.println("[immich] Unknown content length"); http.end(); return false; } // Allocate in PSRAM *outBuffer = (uint8_t*)ps_malloc(contentLength); if (*outBuffer == nullptr) { Serial.printf("[immich] Failed to allocate %d bytes in PSRAM\n", contentLength); http.end(); return false; } WiFiClient* stream = http.getStreamPtr(); size_t bytesRead = 0; while (bytesRead < (size_t)contentLength) { size_t available = stream->available(); if (available > 0) { size_t toRead = min(available, (size_t)(contentLength - bytesRead)); size_t read = stream->readBytes(*outBuffer + bytesRead, toRead); bytesRead += read; } else { delay(1); } if (!http.connected() && bytesRead < (size_t)contentLength) { Serial.println("[immich] Connection lost during download"); free(*outBuffer); *outBuffer = nullptr; http.end(); return false; } } *outSize = bytesRead; http.end(); Serial.printf("[immich] Downloaded %d bytes\n", bytesRead); return true; }