diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 319deb2acf..c71b4151d3 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index dd1c0eb8e4..aa37a294e1 100644 Binary files a/mobile/openapi/doc/PersonApi.md and b/mobile/openapi/doc/PersonApi.md differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index caae449e25..5f664730f0 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1673,7 +1673,13 @@ "responses": { "200": { "content": { - "application/octet-stream": { + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "image/webp": { "schema": { "type": "string", "format": "binary" @@ -2704,7 +2710,7 @@ "responses": { "200": { "content": { - "application/octet-stream": { + "image/jpeg": { "schema": { "type": "string", "format": "binary" diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index c2d3e6b39d..7cd93bd7ed 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -19,7 +19,7 @@ import { ValidationPipe, } from '@nestjs/common'; import { FileFieldsInterceptor } from '@nestjs/platform-express'; -import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { Response as Res } from 'express'; import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard'; import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; @@ -122,7 +122,6 @@ export class AssetController { @SharedLinkRoute() @Get('/file/:id') @Header('Cache-Control', 'private, max-age=86400, no-transform') - @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) serveFile( @AuthUser() authUser: AuthUserDto, @Headers() headers: Record, @@ -136,7 +135,6 @@ export class AssetController { @SharedLinkRoute() @Get('/thumbnail/:id') @Header('Cache-Control', 'private, max-age=86400, no-transform') - @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) getAssetThumbnail( @AuthUser() authUser: AuthUserDto, @Headers() headers: Record, diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 0a1ee8e0e2..26c0ca7bbe 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -256,8 +256,8 @@ export class AssetService { } try { - const thumbnailPath = this.getThumbnailPath(asset, query.format); - return this.streamFile(thumbnailPath, res, headers); + const [thumbnailPath, contentType] = this.getThumbnailPath(asset, query.format); + return this.streamFile(thumbnailPath, res, headers, contentType); } catch (e) { res.header('Cache-Control', 'none'); this.logger.error(`Cannot create read stream for asset ${asset.id}`, 'getAssetThumbnail'); @@ -522,16 +522,17 @@ export class AssetService { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { case GetAssetThumbnailFormatEnum.WEBP: - if (asset.webpPath && asset.webpPath.length > 0) { - return asset.webpPath; + if (asset.webpPath) { + return [asset.webpPath, 'image/webp']; } + this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); case GetAssetThumbnailFormatEnum.JPEG: default: if (!asset.resizePath) { - throw new NotFoundException('resizePath not set'); + throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); } - return asset.resizePath; + return [asset.resizePath, 'image/jpeg']; } } diff --git a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts index 5a8dc06872..ad0e755d6a 100644 --- a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts +++ b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional } from 'class-validator'; +import { IsEnum, IsOptional } from 'class-validator'; export enum GetAssetThumbnailFormatEnum { JPEG = 'JPEG', @@ -8,6 +8,7 @@ export enum GetAssetThumbnailFormatEnum { export class GetAssetThumbnailDto { @IsOptional() + @IsEnum(GetAssetThumbnailFormatEnum) @ApiProperty({ type: String, enum: GetAssetThumbnailFormatEnum, diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 5752304598..2e8135161f 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -7,7 +7,7 @@ import { PersonUpdateDto, } from '@app/domain'; import { Body, Controller, Get, Param, Put, StreamableFile } from '@nestjs/common'; -import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ApiTags } from '@nestjs/swagger'; import { Authenticated, AuthUser } from '../app.guard'; import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @@ -43,7 +43,6 @@ export class PersonController { } @Get(':id/thumbnail') - @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) getPersonThumbnail(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { return this.service.getThumbnail(authUser, id).then(asStreamableFile); }