From a69eecf3bc499450be813b34ad6b2b6af6177af9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 15 Apr 2026 18:34:22 -0400 Subject: [PATCH] chore!: remove without assets (#27835) * chore!: remove without assets * fix: linting and e2e --------- Co-authored-by: Daniel Dietzler --- e2e/src/specs/server/api/album.e2e-spec.ts | 35 +- .../ui/generators/timeline/rest-response.ts | 1 - mobile/openapi/lib/api/albums_api.dart | 15 +- .../openapi/lib/model/album_response_dto.dart | 10 +- open-api/immich-openapi-specs.json | 16 - open-api/typescript-sdk/src/fetch-client.ts | 361 +++++++++--------- server/src/controllers/album.controller.ts | 9 +- server/src/dtos/album-response.dto.spec.ts | 4 +- server/src/dtos/album.dto.ts | 21 +- server/src/dtos/shared-link.dto.ts | 4 +- server/src/services/album.service.spec.ts | 14 +- server/src/services/album.service.ts | 18 +- web/src/lib/modals/AlbumOptionsModal.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.ts | 2 +- web/src/test-data/factories/album-factory.ts | 1 - 16 files changed, 214 insertions(+), 301 deletions(-) diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index a9e90940ab..3725de8d26 100644 --- a/e2e/src/specs/server/api/album.e2e-spec.ts +++ b/e2e/src/specs/server/api/album.e2e-spec.ts @@ -130,12 +130,11 @@ describe('/albums', () => { describe('GET /albums', () => { it("should not show other users' favorites", async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toEqual(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ isFavorite: false })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -304,13 +303,12 @@ describe('/albums', () => { describe('GET /albums/:id', () => { it('should return album info for own album', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], lastModifiedAssetTimestamp: expect.any(String), startDate: expect.any(String), @@ -322,7 +320,7 @@ describe('/albums', () => { it('should return album info for shared album (editor)', async () => { const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=false`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -331,14 +329,14 @@ describe('/albums', () => { it('should return album info for shared album (viewer)', async () => { const { status, body } = await request(app) - .get(`/albums/${user1Albums[3].id}?withoutAssets=false`) + .get(`/albums/${user1Albums[3].id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(200); expect(body).toMatchObject({ id: user1Albums[3].id }); }); - it('should return album info with assets when withoutAssets is undefined', async () => { + it('should return album info', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); @@ -346,25 +344,6 @@ describe('/albums', () => { expect(status).toBe(200); expect(body).toEqual({ ...user1Albums[0], - assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })], - contributorCounts: [{ userId: user1.userId, assetCount: 1 }], - lastModifiedAssetTimestamp: expect.any(String), - startDate: expect.any(String), - endDate: expect.any(String), - albumUsers: expect.any(Array), - shared: true, - }); - }); - - it('should return album info without assets when withoutAssets is true', async () => { - const { status, body } = await request(app) - .get(`/albums/${user1Albums[0].id}?withoutAssets=true`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...user1Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -379,13 +358,12 @@ describe('/albums', () => { await utils.deleteAssets(user1.accessToken, [user1Asset2.id]); const { status, body } = await request(app) - .get(`/albums/${user2Albums[0].id}?withoutAssets=true`) + .get(`/albums/${user2Albums[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ ...user2Albums[0], - assets: [], contributorCounts: [{ userId: user1.userId, assetCount: 1 }], assetCount: 1, lastModifiedAssetTimestamp: expect.any(String), @@ -426,7 +404,6 @@ describe('/albums', () => { shared: false, albumUsers: [], hasSharedLink: false, - assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index de8aa3ee05..3114e3676d 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -427,7 +427,6 @@ export function getAlbum( hasSharedLink: false, isActivityEnabled: true, assetCount: albumAssets.length, - assets: albumAssets, startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined, endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined, diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index fa52ef2eb8..d08d1cba9d 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -315,10 +315,7 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { + Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' .replaceAll('{id}', id); @@ -336,9 +333,6 @@ class AlbumsApi { if (slug != null) { queryParams.addAll(_queryParams('', 'slug', slug)); } - if (withoutAssets != null) { - queryParams.addAll(_queryParams('', 'withoutAssets', withoutAssets)); - } const contentTypes = []; @@ -365,11 +359,8 @@ class AlbumsApi { /// * [String] key: /// /// * [String] slug: - /// - /// * [bool] withoutAssets: - /// Exclude assets from response - Future getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { - final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, ); + Future getAlbumInfo(String id, { String? key, String? slug, }) async { + final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index ca0c087027..348e25ddaf 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -17,7 +17,6 @@ class AlbumResponseDto { required this.albumThumbnailAssetId, this.albumUsers = const [], required this.assetCount, - this.assets = const [], this.contributorCounts = const [], required this.createdAt, required this.description, @@ -48,8 +47,6 @@ class AlbumResponseDto { /// Maximum value: 9007199254740991 int assetCount; - List assets; - List contributorCounts; /// Creation date @@ -119,7 +116,6 @@ class AlbumResponseDto { other.albumThumbnailAssetId == albumThumbnailAssetId && _deepEquality.equals(other.albumUsers, albumUsers) && other.assetCount == assetCount && - _deepEquality.equals(other.assets, assets) && _deepEquality.equals(other.contributorCounts, contributorCounts) && other.createdAt == createdAt && other.description == description && @@ -142,7 +138,6 @@ class AlbumResponseDto { (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumUsers.hashCode) + (assetCount.hashCode) + - (assets.hashCode) + (contributorCounts.hashCode) + (createdAt.hashCode) + (description.hashCode) + @@ -159,7 +154,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -171,7 +166,6 @@ class AlbumResponseDto { } json[r'albumUsers'] = this.albumUsers; json[r'assetCount'] = this.assetCount; - json[r'assets'] = this.assets; json[r'contributorCounts'] = this.contributorCounts; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; @@ -218,7 +212,6 @@ class AlbumResponseDto { albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']), assetCount: mapValueOfType(json, r'assetCount')!, - assets: AssetResponseDto.listFromJson(json[r'assets']), contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']), createdAt: mapDateTime(json, r'createdAt', r'')!, description: mapValueOfType(json, r'description')!, @@ -284,7 +277,6 @@ class AlbumResponseDto { 'albumThumbnailAssetId', 'albumUsers', 'assetCount', - 'assets', 'createdAt', 'description', 'hasSharedLink', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 99d77f9b24..33a692474a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1956,15 +1956,6 @@ "schema": { "type": "string" } - }, - { - "name": "withoutAssets", - "required": false, - "in": "query", - "description": "Exclude assets from response", - "schema": { - "type": "boolean" - } } ], "responses": { @@ -15305,12 +15296,6 @@ "minimum": 0, "type": "integer" }, - "assets": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - }, "contributorCounts": { "items": { "$ref": "#/components/schemas/ContributorCountResponseDto" @@ -15378,7 +15363,6 @@ "albumThumbnailAssetId", "albumUsers", "assetCount", - "assets", "createdAt", "description", "hasSharedLink", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index da38eaeace..0875715beb 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -446,172 +446,6 @@ export type AlbumUserResponseDto = { role: AlbumUserRole; user: UserResponseDto; }; -export type ExifResponseDto = { - /** City name */ - city?: string | null; - /** Country name */ - country?: string | null; - /** Original date/time */ - dateTimeOriginal?: string | null; - /** Image description */ - description?: string | null; - /** Image height in pixels */ - exifImageHeight?: number | null; - /** Image width in pixels */ - exifImageWidth?: number | null; - /** Exposure time */ - exposureTime?: string | null; - /** F-number (aperture) */ - fNumber?: number | null; - /** File size in bytes */ - fileSizeInByte?: number | null; - /** Focal length in mm */ - focalLength?: number | null; - /** ISO sensitivity */ - iso?: number | null; - /** GPS latitude */ - latitude?: number | null; - /** Lens model */ - lensModel?: string | null; - /** GPS longitude */ - longitude?: number | null; - /** Camera make */ - make?: string | null; - /** Camera model */ - model?: string | null; - /** Modification date/time */ - modifyDate?: string | null; - /** Image orientation */ - orientation?: string | null; - /** Projection type */ - projectionType?: string | null; - /** Rating */ - rating?: number | null; - /** State/province name */ - state?: string | null; - /** Time zone */ - timeZone?: string | null; -}; -export type AssetFaceWithoutPersonResponseDto = { - /** Bounding box X1 coordinate */ - boundingBoxX1: number; - /** Bounding box X2 coordinate */ - boundingBoxX2: number; - /** Bounding box Y1 coordinate */ - boundingBoxY1: number; - /** Bounding box Y2 coordinate */ - boundingBoxY2: number; - /** Face ID */ - id: string; - /** Image height in pixels */ - imageHeight: number; - /** Image width in pixels */ - imageWidth: number; - sourceType?: SourceType; -}; -export type PersonWithFacesResponseDto = { - /** Person date of birth */ - birthDate: string | null; - /** Person color (hex) */ - color?: string; - faces: AssetFaceWithoutPersonResponseDto[]; - /** Person ID */ - id: string; - /** Is favorite */ - isFavorite?: boolean; - /** Is hidden */ - isHidden: boolean; - /** Person name */ - name: string; - /** Thumbnail path */ - thumbnailPath: string; - /** Last update date */ - updatedAt?: string; -}; -export type AssetStackResponseDto = { - /** Number of assets in stack */ - assetCount: number; - /** Stack ID */ - id: string; - /** Primary asset ID */ - primaryAssetId: string; -}; -export type TagResponseDto = { - /** Tag color (hex) */ - color?: string; - /** Creation date */ - createdAt: string; - /** Tag ID */ - id: string; - /** Tag name */ - name: string; - /** Parent tag ID */ - parentId?: string; - /** Last update date */ - updatedAt: string; - /** Tag value (full path) */ - value: string; -}; -export type AssetResponseDto = { - /** Base64 encoded SHA1 hash */ - checksum: string; - /** The UTC timestamp when the asset was originally uploaded to Immich. */ - createdAt: string; - /** Duplicate group ID */ - duplicateId?: string | null; - /** Video duration (for videos) */ - duration: string; - exifInfo?: ExifResponseDto; - /** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */ - fileCreatedAt: string; - /** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */ - fileModifiedAt: string; - /** Whether asset has metadata */ - hasMetadata: boolean; - /** Asset height */ - height: number | null; - /** Asset ID */ - id: string; - /** Is archived */ - isArchived: boolean; - /** Is edited */ - isEdited: boolean; - /** Is favorite */ - isFavorite: boolean; - /** Is offline */ - isOffline: boolean; - /** Is trashed */ - isTrashed: boolean; - /** Library ID */ - libraryId?: string | null; - /** Live photo video ID */ - livePhotoVideoId?: string | null; - /** The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months. */ - localDateTime: string; - /** Original file name */ - originalFileName: string; - /** Original MIME type */ - originalMimeType?: string; - /** Original file path */ - originalPath: string; - owner?: UserResponseDto; - /** Owner user ID */ - ownerId: string; - people?: PersonWithFacesResponseDto[]; - /** Is resized */ - resized?: boolean; - stack?: (AssetStackResponseDto) | null; - tags?: TagResponseDto[]; - /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ - thumbhash: string | null; - "type": AssetTypeEnum; - unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; - /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ - updatedAt: string; - visibility: AssetVisibility; - /** Asset width */ - width: number | null; -}; export type ContributorCountResponseDto = { /** Number of assets contributed */ assetCount: number; @@ -626,7 +460,6 @@ export type AlbumResponseDto = { albumUsers: AlbumUserResponseDto[]; /** Number of assets */ assetCount: number; - assets: AssetResponseDto[]; contributorCounts?: ContributorCountResponseDto[]; /** Creation date */ createdAt: string; @@ -910,6 +743,172 @@ export type AssetMetadataBulkResponseDto = { [key: string]: any; }; }; +export type ExifResponseDto = { + /** City name */ + city?: string | null; + /** Country name */ + country?: string | null; + /** Original date/time */ + dateTimeOriginal?: string | null; + /** Image description */ + description?: string | null; + /** Image height in pixels */ + exifImageHeight?: number | null; + /** Image width in pixels */ + exifImageWidth?: number | null; + /** Exposure time */ + exposureTime?: string | null; + /** F-number (aperture) */ + fNumber?: number | null; + /** File size in bytes */ + fileSizeInByte?: number | null; + /** Focal length in mm */ + focalLength?: number | null; + /** ISO sensitivity */ + iso?: number | null; + /** GPS latitude */ + latitude?: number | null; + /** Lens model */ + lensModel?: string | null; + /** GPS longitude */ + longitude?: number | null; + /** Camera make */ + make?: string | null; + /** Camera model */ + model?: string | null; + /** Modification date/time */ + modifyDate?: string | null; + /** Image orientation */ + orientation?: string | null; + /** Projection type */ + projectionType?: string | null; + /** Rating */ + rating?: number | null; + /** State/province name */ + state?: string | null; + /** Time zone */ + timeZone?: string | null; +}; +export type AssetFaceWithoutPersonResponseDto = { + /** Bounding box X1 coordinate */ + boundingBoxX1: number; + /** Bounding box X2 coordinate */ + boundingBoxX2: number; + /** Bounding box Y1 coordinate */ + boundingBoxY1: number; + /** Bounding box Y2 coordinate */ + boundingBoxY2: number; + /** Face ID */ + id: string; + /** Image height in pixels */ + imageHeight: number; + /** Image width in pixels */ + imageWidth: number; + sourceType?: SourceType; +}; +export type PersonWithFacesResponseDto = { + /** Person date of birth */ + birthDate: string | null; + /** Person color (hex) */ + color?: string; + faces: AssetFaceWithoutPersonResponseDto[]; + /** Person ID */ + id: string; + /** Is favorite */ + isFavorite?: boolean; + /** Is hidden */ + isHidden: boolean; + /** Person name */ + name: string; + /** Thumbnail path */ + thumbnailPath: string; + /** Last update date */ + updatedAt?: string; +}; +export type AssetStackResponseDto = { + /** Number of assets in stack */ + assetCount: number; + /** Stack ID */ + id: string; + /** Primary asset ID */ + primaryAssetId: string; +}; +export type TagResponseDto = { + /** Tag color (hex) */ + color?: string; + /** Creation date */ + createdAt: string; + /** Tag ID */ + id: string; + /** Tag name */ + name: string; + /** Parent tag ID */ + parentId?: string; + /** Last update date */ + updatedAt: string; + /** Tag value (full path) */ + value: string; +}; +export type AssetResponseDto = { + /** Base64 encoded SHA1 hash */ + checksum: string; + /** The UTC timestamp when the asset was originally uploaded to Immich. */ + createdAt: string; + /** Duplicate group ID */ + duplicateId?: string | null; + /** Video duration (for videos) */ + duration: string; + exifInfo?: ExifResponseDto; + /** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */ + fileCreatedAt: string; + /** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */ + fileModifiedAt: string; + /** Whether asset has metadata */ + hasMetadata: boolean; + /** Asset height */ + height: number | null; + /** Asset ID */ + id: string; + /** Is archived */ + isArchived: boolean; + /** Is edited */ + isEdited: boolean; + /** Is favorite */ + isFavorite: boolean; + /** Is offline */ + isOffline: boolean; + /** Is trashed */ + isTrashed: boolean; + /** Library ID */ + libraryId?: string | null; + /** Live photo video ID */ + livePhotoVideoId?: string | null; + /** The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months. */ + localDateTime: string; + /** Original file name */ + originalFileName: string; + /** Original MIME type */ + originalMimeType?: string; + /** Original file path */ + originalPath: string; + owner?: UserResponseDto; + /** Owner user ID */ + ownerId: string; + people?: PersonWithFacesResponseDto[]; + /** Is resized */ + resized?: boolean; + stack?: (AssetStackResponseDto) | null; + tags?: TagResponseDto[]; + /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ + thumbhash: string | null; + "type": AssetTypeEnum; + unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; + /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ + updatedAt: string; + visibility: AssetVisibility; + /** Asset width */ + width: number | null; +}; export type UpdateAssetDto = { /** Original date and time */ dateTimeOriginal?: string; @@ -3668,19 +3667,17 @@ export function deleteAlbum({ id }: { /** * Retrieve an album */ -export function getAlbumInfo({ id, key, slug, withoutAssets }: { +export function getAlbumInfo({ id, key, slug }: { id: string; key?: string; slug?: string; - withoutAssets?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: AlbumResponseDto; }>(`/albums/${encodeURIComponent(id)}${QS.query(QS.explode({ key, - slug, - withoutAssets + slug }))}`, { ...opts })); @@ -6729,17 +6726,6 @@ export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" } -export enum SourceType { - MachineLearning = "machine-learning", - Exif = "exif", - Manual = "manual" -} -export enum AssetTypeEnum { - Image = "IMAGE", - Video = "VIDEO", - Audio = "AUDIO", - Other = "OTHER" -} export enum BulkIdErrorReason { Duplicate = "duplicate", NoPermission = "no_permission", @@ -6923,6 +6909,17 @@ export enum AssetJobName { RegenerateThumbnail = "regenerate-thumbnail", TranscodeVideo = "transcode-video" } +export enum SourceType { + MachineLearning = "machine-learning", + Exif = "exif", + Manual = "manual" +} +export enum AssetTypeEnum { + Image = "IMAGE", + Video = "VIDEO", + Audio = "AUDIO", + Other = "OTHER" +} export enum AssetEditAction { Crop = "crop", Rotate = "rotate", diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 62e3f53ad2..90a8fa5a25 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -3,7 +3,6 @@ import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AddUsersDto, - AlbumInfoDto, AlbumResponseDto, AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, @@ -66,12 +65,8 @@ export class AlbumController { description: 'Retrieve information about a specific album by its ID.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getAlbumInfo( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @Query() dto: AlbumInfoDto, - ): Promise { - return this.service.get(auth, id, dto); + getAlbumInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); } @Patch(':id') diff --git a/server/src/dtos/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts index e82067580b..c03662288a 100644 --- a/server/src/dtos/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -10,13 +10,13 @@ describe('mapAlbum', () => { .asset({ localDateTime: endDate }, (builder) => builder.exif()) .asset({ localDateTime: startDate }, (builder) => builder.exif()) .build(); - const dto = mapAlbum(getForAlbum(album), false); + const dto = mapAlbum(getForAlbum(album)); expect(dto.startDate).toEqual(startDate.toISOString()); expect(dto.endDate).toEqual(endDate.toISOString()); }); it('should not set start and end dates for empty assets', () => { - const dto = mapAlbum(getForAlbum(AlbumFactory.create()), false); + const dto = mapAlbum(getForAlbum(AlbumFactory.create())); expect(dto.startDate).toBeUndefined(); expect(dto.endDate).toBeUndefined(); }); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 1514809838..519094cd94 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -3,8 +3,7 @@ import _ from 'lodash'; import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseSchema, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; -import { AuthDto } from 'src/dtos/auth.dto'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { MaybeDehydrated } from 'src/types'; @@ -12,12 +11,6 @@ import { asDateString } from 'src/utils/date'; import { stringToBool } from 'src/validation'; import z from 'zod'; -const AlbumInfoSchema = z - .object({ - withoutAssets: stringToBool.optional().describe('Exclude assets from response'), - }) - .meta({ id: 'AlbumInfoDto' }); - const AlbumUserAddSchema = z .object({ userId: z.uuidv4().describe('User ID'), @@ -122,7 +115,6 @@ export const AlbumResponseSchema = z shared: z.boolean().describe('Is shared album'), albumUsers: z.array(AlbumUserResponseSchema), hasSharedLink: z.boolean().describe('Has shared link'), - assets: z.array(AssetResponseSchema), owner: UserResponseSchema, assetCount: z.int().min(0).describe('Number of assets'), // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. @@ -141,7 +133,6 @@ export const AlbumResponseSchema = z }) .meta({ id: 'AlbumResponseDto' }); -export class AlbumInfoDto extends createZodDto(AlbumInfoSchema) {} export class AddUsersDto extends createZodDto(AddUsersSchema) {} export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {} export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {} @@ -170,11 +161,7 @@ export type MapAlbumDto = { order: AssetOrder; }; -export const mapAlbum = ( - entity: MaybeDehydrated, - withAssets: boolean, - auth?: AuthDto, -): AlbumResponseDto => { +export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto => { const albumUsers: AlbumUserResponseDto[] = []; if (entity.albumUsers) { @@ -215,12 +202,8 @@ export const mapAlbum = ( hasSharedLink, startDate: asDateString(startDate), endDate: asDateString(endDate), - assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })), assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, order: entity.order, }; }; - -export const mapAlbumWithAssets = (entity: MaybeDehydrated) => mapAlbum(entity, true); -export const mapAlbumWithoutAssets = (entity: MaybeDehydrated) => mapAlbum(entity, false); diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index aa58c0833e..2e466c5014 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,7 +1,7 @@ import { createZodDto } from 'nestjs-zod'; import { SharedLink } from 'src/database'; import { HistoryBuilder } from 'src/decorators'; -import { AlbumResponseSchema, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AlbumResponseSchema, mapAlbum } from 'src/dtos/album.dto'; import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkTypeSchema } from 'src/enum'; import { emptyStringToNull, isoDatetimeToDate } from 'src/validation'; @@ -96,7 +96,7 @@ export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetad createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })), - album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, + album: sharedLink.album ? mapAlbum(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, showMetadata: sharedLink.showExif, diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index d24786fbb6..4f5d4edd00 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -563,9 +563,9 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(AuthFactory.create(album.owner), album.id, {}); + await sut.get(AuthFactory.create(album.owner), album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id])); }); @@ -584,9 +584,9 @@ describe(AlbumService.name, () => { ]); const auth = AuthFactory.from().sharedLink().build(); - await sut.get(auth, album.id, {}); + await sut.get(auth, album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id])); }); @@ -605,9 +605,9 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(AuthFactory.create(user), album.id, {}); + await sut.get(AuthFactory.create(user), album.id); - expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( user.id, new Set([album.id]), @@ -617,7 +617,7 @@ describe(AlbumService.name, () => { it('should throw an error for no access', async () => { const auth = AuthFactory.create(); - await expect(sut.get(auth, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(auth, 'album-123')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['album-123'])); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 1e93b8eace..8142bfeff5 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { AddUsersDto, - AlbumInfoDto, AlbumResponseDto, AlbumsAddAssetsDto, AlbumsAddAssetsResponseDto, @@ -10,8 +9,6 @@ import { GetAlbumsDto, mapAlbum, MapAlbumDto, - mapAlbumWithAssets, - mapAlbumWithoutAssets, UpdateAlbumDto, UpdateAlbumUserDto, } from 'src/dtos/album.dto'; @@ -64,7 +61,7 @@ export class AlbumService extends BaseService { } return albums.map((album) => ({ - ...mapAlbumWithoutAssets(album), + ...mapAlbum(album), sharedLinks: undefined, startDate: asDateString(albumMetadata[album.id]?.startDate ?? undefined), endDate: asDateString(albumMetadata[album.id]?.endDate ?? undefined), @@ -74,11 +71,10 @@ export class AlbumService extends BaseService { })); } - async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { + async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] }); await this.albumRepository.updateThumbnails(); - const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; - const album = await this.findOrFail(id, { withAssets }); + const album = await this.findOrFail(id, { withAssets: false }); const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); const hasSharedUsers = album.albumUsers && album.albumUsers.length > 0; @@ -86,7 +82,7 @@ export class AlbumService extends BaseService { const isShared = hasSharedUsers || hasSharedLink; return { - ...mapAlbum(album, withAssets, auth), + ...mapAlbum(album), startDate: asDateString(albumMetadataForIds?.startDate ?? undefined), endDate: asDateString(albumMetadataForIds?.endDate ?? undefined), assetCount: albumMetadataForIds?.assetCount ?? 0, @@ -144,7 +140,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id: album.id, userId }); } - return mapAlbumWithAssets(album); + return mapAlbum(album); } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { @@ -167,7 +163,7 @@ export class AlbumService extends BaseService { order: dto.order, }); - return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets }); + return mapAlbum({ ...updatedAlbum, assets: album.assets }); } async delete(auth: AuthDto, id: string): Promise { @@ -305,7 +301,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id, userId }); } - return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); + return this.findOrFail(id, { withAssets: true }).then(mapAlbum); } async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise { diff --git a/web/src/lib/modals/AlbumOptionsModal.svelte b/web/src/lib/modals/AlbumOptionsModal.svelte index 4553f022df..80961969c5 100644 --- a/web/src/lib/modals/AlbumOptionsModal.svelte +++ b/web/src/lib/modals/AlbumOptionsModal.svelte @@ -39,7 +39,7 @@ }; const refreshAlbum = async () => { - album = await getAlbumInfo({ id: album.id, withoutAssets: true }); + album = await getAlbumInfo({ id: album.id }); }; const onAlbumUserDelete = async ({ userId }: { userId: string }) => { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3532897ad9..dc3a7b60d4 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -133,7 +133,7 @@ }; const refreshAlbum = async () => { - album = await getAlbumInfo({ id: album.id, withoutAssets: true }); + album = await getAlbumInfo({ id: album.id }); }; const setModeToView = async () => { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts index 359bb84ca5..623c39b152 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -4,7 +4,7 @@ import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true }); + const album = await getAlbumInfo({ id: params.albumId }); return { album, diff --git a/web/src/test-data/factories/album-factory.ts b/web/src/test-data/factories/album-factory.ts index 7ea3715f73..7ad44b7ac5 100644 --- a/web/src/test-data/factories/album-factory.ts +++ b/web/src/test-data/factories/album-factory.ts @@ -8,7 +8,6 @@ export const albumFactory = Sync.makeFactory({ description: '', albumThumbnailAssetId: null, assetCount: Sync.each((index) => index % 5), - assets: [], createdAt: Sync.each(() => faker.date.past().toISOString()), updatedAt: Sync.each(() => faker.date.past().toISOString()), id: Sync.each(() => faker.string.uuid()),