* fix(immich): use thumbnail endpoint instead of original for album carousel Switches from /api/assets/:id/original to /api/assets/:id/thumbnail to fix HEIC image incompatibility (thumbnails are always JPEG), reduce bandwidth, and improve loading speed. Closes #6388 * working on immich * Improve image proxy error handling * fix(image-proxy): use trusted fetch response type * docs(immich): require Asset View permission
155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import {
|
|
AssetMediaSize,
|
|
getAlbumInfo,
|
|
getAllAlbums,
|
|
getMyUser,
|
|
getServerStatistics,
|
|
init,
|
|
searchAssets,
|
|
searchUsers,
|
|
} from "@immich/sdk";
|
|
import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk";
|
|
|
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
import { ImageProxy } from "@homarr/image-proxy";
|
|
|
|
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
|
|
import { Integration } from "../base/integration";
|
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
|
|
|
export interface ImmichServerStats {
|
|
userCount: number;
|
|
photoCount: number;
|
|
videoCount: number;
|
|
totalLibraryUsageInBytes: number;
|
|
}
|
|
|
|
export interface ImmichAlbum {
|
|
id: string;
|
|
albumName: string;
|
|
assets: ImmichAsset[];
|
|
}
|
|
|
|
export interface ImmichAsset {
|
|
id: string;
|
|
originalPath: string;
|
|
thumbhash: string | null;
|
|
fileModifiedAt: string;
|
|
fileCreatedAt: string;
|
|
updatedAt: string;
|
|
type: "IMAGE" | "VIDEO" | "AUDIO" | "OTHER";
|
|
publicLink: string;
|
|
}
|
|
|
|
const logger = createLogger({ module: "immich-integration" });
|
|
|
|
export class ImmichIntegration extends Integration {
|
|
constructor(integration: IntegrationInput) {
|
|
super(integration);
|
|
}
|
|
|
|
public async getServerStatsAsync(): Promise<ImmichServerStats> {
|
|
this.initClient();
|
|
const [statistics, users] = await Promise.all([
|
|
getServerStatistics(this.getRequestOptions()),
|
|
searchUsers(this.getRequestOptions()),
|
|
]);
|
|
return {
|
|
photoCount: statistics.photos,
|
|
userCount: users.length,
|
|
totalLibraryUsageInBytes: statistics.usage,
|
|
videoCount: statistics.videos,
|
|
};
|
|
}
|
|
|
|
public async getAlbumAsync(albumId: string): Promise<ImmichAlbum> {
|
|
this.initClient();
|
|
const requestOptions = this.getRequestOptions();
|
|
const [album, albumAssets] = await Promise.all([
|
|
getAlbumInfo({ id: albumId }, requestOptions),
|
|
this.searchAlbumAssetsAsync(albumId),
|
|
]);
|
|
const imageProxy = new ImageProxy();
|
|
return {
|
|
albumName: album.albumName,
|
|
assets: await Promise.all(
|
|
albumAssets.map(async (asset) => {
|
|
const publicLink = await imageProxy.createImageAsync(
|
|
// This is the URL generated by the SDK's viewAsset call. Preview images are
|
|
// browser-compatible while avoiding the large original asset download.
|
|
this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(),
|
|
{
|
|
"x-api-key": this.getSecretValue("apiKey"),
|
|
},
|
|
);
|
|
return {
|
|
id: asset.id,
|
|
type: asset.type,
|
|
thumbhash: asset.thumbhash,
|
|
fileCreatedAt: asset.fileCreatedAt,
|
|
fileModifiedAt: asset.fileModifiedAt,
|
|
originalPath: asset.originalPath,
|
|
updatedAt: asset.updatedAt,
|
|
publicLink,
|
|
};
|
|
}),
|
|
),
|
|
id: album.id,
|
|
};
|
|
}
|
|
|
|
public async getAlbumsAsync(): Promise<
|
|
{
|
|
id: string;
|
|
albumName: string;
|
|
assetCount: number;
|
|
}[]
|
|
> {
|
|
this.initClient();
|
|
const albums = await getAllAlbums({}, this.getRequestOptions());
|
|
return albums.map((album) => ({ id: album.id, albumName: album.albumName, assetCount: album.assetCount }));
|
|
}
|
|
|
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
|
this.initClient();
|
|
const user = await getMyUser(this.getRequestOptions(input.fetchAsync));
|
|
logger.debug(`Logged in as ${user.name} (${user.id})`);
|
|
return { success: true };
|
|
}
|
|
|
|
private async searchAlbumAssetsAsync(albumId: string): Promise<AssetResponseDto[]> {
|
|
const requestOptions = this.getRequestOptions();
|
|
const assets: AssetResponseDto[] = [];
|
|
let nextPage: string | null = null;
|
|
|
|
do {
|
|
const metadataSearchDto: MetadataSearchDto = { albumIds: [albumId] };
|
|
if (nextPage !== null) {
|
|
metadataSearchDto.page = Number(nextPage);
|
|
}
|
|
const searchResult = await searchAssets({ metadataSearchDto }, requestOptions);
|
|
assets.push(...searchResult.assets.items);
|
|
nextPage = searchResult.assets.nextPage;
|
|
} while (nextPage !== null);
|
|
|
|
return assets;
|
|
}
|
|
|
|
private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) {
|
|
return {
|
|
// Undici and node types are not the same
|
|
fetch: fetchAsync as unknown as typeof fetch,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sets the credentials and prepares the client for calls.
|
|
* Must be called before any other functions.
|
|
* @private
|
|
*/
|
|
private initClient() {
|
|
init({ baseUrl: this.url("/api").toString(), apiKey: this.getSecretValue("apiKey") });
|
|
}
|
|
}
|