New getAllAlbumsSlim endpoint

- Add new endpoint
- Add slim option to albumService.getAll (default false)
- Use getAllAlbumsSlim in search-albums-section
This commit is contained in:
CJPeckover
2025-07-14 13:44:05 -04:00
parent 4a6ba4a40a
commit 419175d79b
6 changed files with 113 additions and 11 deletions
+54
View File
@@ -870,6 +870,60 @@
]
}
},
"/albums/slim": {
"get": {
"operationId": "getAllAlbumsSlim",
"parameters": [
{
"name": "assetId",
"required": false,
"in": "query",
"description": "Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "shared",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/AlbumResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Albums"
]
}
},
"/albums/statistics": {
"get": {
"operationId": "getAlbumStatistics",
@@ -1790,6 +1790,20 @@ export function createAlbum({ createAlbumDto }: {
body: createAlbumDto
})));
}
export function getAllAlbumsSlim({ assetId, shared }: {
assetId?: string;
shared?: boolean;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AlbumResponseDto[];
}>(`/albums/slim${QS.query(QS.explode({
assetId,
shared
}))}`, {
...opts
}));
}
export function getAlbumStatistics(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
@@ -37,6 +37,25 @@ describe(AlbumController.name, () => {
});
});
describe('GET /albums/slim', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/albums/slim');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/albums/slim?shared=invalid');
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value']));
});
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/albums/slim?assetId=invalid');
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID']));
});
});
describe('GET /albums/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`);
@@ -28,6 +28,13 @@ export class AlbumController {
return this.service.getAll(auth, query);
}
@Get('slim')
@Authenticated({ permission: Permission.ALBUM_READ })
getAllAlbumsSlim(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(auth, query, true);
//asdf
}
@Post()
@Authenticated({ permission: Permission.ALBUM_CREATE })
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> {
+17 -9
View File
@@ -37,7 +37,11 @@ export class AlbumService extends BaseService {
};
}
async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
async getAll(
{ user: { id: ownerId } }: AuthDto,
{ assetId, shared }: GetAlbumsDto,
slim: boolean = false,
): Promise<AlbumResponseDto[]> {
await this.albumRepository.updateThumbnails();
let albums: MapAlbumDto[];
@@ -53,20 +57,24 @@ export class AlbumService extends BaseService {
// Get asset count for each album. Then map the result to an object:
// { [albumId]: assetCount }
const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
const albumMetadata: Record<string, AlbumAssetCount> = {};
for (const metadata of results) {
albumMetadata[metadata.albumId] = metadata;
if (!slim) {
const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
for (const metadata of results) {
albumMetadata[metadata.albumId] = metadata;
}
}
return albums.map((album) => ({
...mapAlbumWithoutAssets(album),
sharedLinks: undefined,
startDate: albumMetadata[album.id]?.startDate ?? undefined,
endDate: albumMetadata[album.id]?.endDate ?? undefined,
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
...(!slim && {
startDate: albumMetadata[album.id]?.startDate ?? undefined,
endDate: albumMetadata[album.id]?.endDate ?? undefined,
assetCount: albumMetadata[album.id]?.assetCount ?? 0,
// lastModifiedAssetTimestamp is only used in mobile app, please remove if not need
lastModifiedAssetTimestamp: albumMetadata[album.id]?.lastModifiedAssetTimestamp ?? undefined,
}),
}));
}
@@ -2,7 +2,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
import { getAssetThumbnailUrl } from '$lib/utils';
import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
import { getAllAlbumsSlim, type AlbumResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -23,7 +23,7 @@
);
onMount(async () => {
allAlbums = await getAllAlbums({});
allAlbums = await getAllAlbumsSlim({});
});
const handleSelect = (option?: ComboBoxOption) => {