mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
chore!: remove without assets (#27835)
* chore!: remove without assets * fix: linting and e2e --------- Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+3
-12
@@ -315,10 +315,7 @@ class AlbumsApi {
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [bool] withoutAssets:
|
||||
/// Exclude assets from response
|
||||
Future<Response> getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async {
|
||||
Future<Response> 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 = <String>[];
|
||||
|
||||
@@ -365,11 +359,8 @@ class AlbumsApi {
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [bool] withoutAssets:
|
||||
/// Exclude assets from response
|
||||
Future<AlbumResponseDto?> getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async {
|
||||
final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, );
|
||||
Future<AlbumResponseDto?> 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));
|
||||
}
|
||||
|
||||
+1
-9
@@ -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<AssetResponseDto> assets;
|
||||
|
||||
List<ContributorCountResponseDto> 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<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -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<String>(json, r'albumThumbnailAssetId'),
|
||||
albumUsers: AlbumUserResponseDto.listFromJson(json[r'albumUsers']),
|
||||
assetCount: mapValueOfType<int>(json, r'assetCount')!,
|
||||
assets: AssetResponseDto.listFromJson(json[r'assets']),
|
||||
contributorCounts: ContributorCountResponseDto.listFromJson(json[r'contributorCounts']),
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
@@ -284,7 +277,6 @@ class AlbumResponseDto {
|
||||
'albumThumbnailAssetId',
|
||||
'albumUsers',
|
||||
'assetCount',
|
||||
'assets',
|
||||
'createdAt',
|
||||
'description',
|
||||
'hasSharedLink',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<AlbumResponseDto> {
|
||||
return this.service.get(auth, id, dto);
|
||||
getAlbumInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AlbumResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<MapAlbumDto>,
|
||||
withAssets: boolean,
|
||||
auth?: AuthDto,
|
||||
): AlbumResponseDto => {
|
||||
export const mapAlbum = (entity: MaybeDehydrated<MapAlbumDto>): 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<MapAlbumDto>) => mapAlbum(entity, true);
|
||||
export const mapAlbumWithoutAssets = (entity: MaybeDehydrated<MapAlbumDto>) => mapAlbum(entity, false);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<AlbumResponseDto> {
|
||||
async get(auth: AuthDto, id: string): Promise<AlbumResponseDto> {
|
||||
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<AlbumResponseDto> {
|
||||
@@ -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<void> {
|
||||
@@ -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<void> {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@
|
||||
};
|
||||
|
||||
const refreshAlbum = async () => {
|
||||
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
|
||||
album = await getAlbumInfo({ id: album.id });
|
||||
};
|
||||
|
||||
const setModeToView = async () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,6 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
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()),
|
||||
|
||||
Reference in New Issue
Block a user