From 1df1b85aa8e23a54f1d93514611f66bd0f8d169e Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 18 Dec 2024 11:36:15 +0100 Subject: [PATCH] asset count instead of statistics --- mobile/openapi/README.md | Bin 32447 -> 32366 bytes mobile/openapi/lib/api.dart | Bin 11793 -> 11747 bytes mobile/openapi/lib/api/libraries_api.dart | Bin 12442 -> 12351 bytes mobile/openapi/lib/api_client.dart | Bin 30193 -> 30095 bytes .../lib/model/library_stats_response_dto.dart | Bin 3566 -> 0 bytes open-api/immich-openapi-specs.json | 58 +++++------------- open-api/typescript-sdk/src/fetch-client.ts | 26 +++----- server/src/controllers/library.controller.ts | 6 +- server/src/interfaces/asset.interface.ts | 2 +- server/src/repositories/asset.repository.ts | 2 +- server/src/services/asset.service.ts | 7 ++- server/src/services/job.service.ts | 2 +- server/src/services/library.service.ts | 23 +++---- server/src/services/trash.service.ts | 2 +- .../admin/library-management/+page.svelte | 40 +++--------- 15 files changed, 60 insertions(+), 108 deletions(-) delete mode 100644 mobile/openapi/lib/model/library_stats_response_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b336b1bfb6f400ed5ad7dc9aa9c1e537d6225e17..e03f4dac77564d7cbf9757e60ee871841f1b1690 100644 GIT binary patch delta 65 zcmdo0m+{>n#tqK$llM!pvO4FN=9NsYmy_g8Pc2C-E>0~;28%qDmtlpl@)aZ|JIE(& OmRAt8-<%g8C=UQ>^%%4O delta 99 zcmaF&hjIU3#tqK$lh@0~PF^F=#T8tVSdv*>l9^mQxluuK@_Bi#$pu>CljY?(C%>0x t<<>7o$j2y1Z01oAwC6}FN-U}bYMeZwRAf>-I}6B+$q9j?o2BD}BpCnz diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 73eb02d89ed7a568bc94067df317179576373af9..3fccede06eb50266b36b8ff41623b55f1e72bd6e 100644 GIT binary patch delta 12 TcmbOj^Ei4#mB{85BAJo^CvgQ^ delta 22 dcmaDHJuzlOl?ZE6QDRZ$``M2-*Mu diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart index 36d98d9a88a78f4efb0375988ca67fdfc01d1508..6010b7a9fcf56f4b32e7b25c8cd0a978c683a6d1 100644 GIT binary patch delta 138 zcmbQ0xIbZop%9yXa(-!E$z(&JGA_sB;?xpnFxOvMd@`?m7%N2bo^s^m1M>GK7f4+Z z&MVEew^K+@EkRO!PC|0>d0p|z0{kqT2s8F8F4=s5--~hb2Yqi|pf+_KO@+i_1t4qk SA9m}_azg)EH^<9a3jqKnb~Cd8 delta 203 zcmdm=Fe`C`q0nSsj^fST5|ylz<%C4Iic1npGK)(xlZz)iY9>u~;AWq^K!|s8p9DKs zFhc5|rugJ@LSaa%H#15KF{y);6bGdi7v$#^r@EBn+uJFmr`!-^+=WOv)lB4;fG0Bz4n!vFvP diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a6f8d551da81c1d006119ad3cedcd3256cae9d25..aa5db6589b4628f432b636f3e385e200d19e8f16 100644 GIT binary patch delta 14 VcmezPnz8>i;|6Yv&AApWwg5KJ24w&M delta 36 ncmeBw&G_*(;|6XE)}*4uqRPp4eN@rn|P^Gc??BXShv*0BV+R9t=Rhw(=Q0zy3i0plU@%XGwQ#O(jWF9QC!D>MHbe)+IilwhX^tS8fUm|} zq$5fssNVee;h#YDWBT$X=gPKf1fnu(=)5hVh`>K|-j$F<;2%2gDrh3;NswS#Ne79r z$zOVb&`vn0n)1Z-OwOgXNDE>rk}r`~Sr!|4h{H<#2!8YX9Ou(*4)N)>53bpNQim&N zRJOwG{lkc%ANCMy=7Wk3{cqqwiw-#^>{OyqbjUwJiA!|oL%12T>(3&8=RF0m*kY5a z`2?+evN}W8p0`VtAv?d}7Si#F^!>wI1_D@OW`;hN;SCD682?C!BWz@N`ACGrZbW$b zNW`bo$W*1LZZq7!9299iP^S@fRvINkf0$U5`!n!A<82QoEszZ;< z5j!L~Pn%;{i~K7gJY8OIbNLS2b}}OF1yED#=)liTQe)=q*YaFzD>0Y$D|z;{l{~rj zYn6m-)JjAjIL(rh#EP1db!`#F9*icCD(lXWZ=E80Q0{qfuZ-S^+mz0WrAVy|9;%x8 ziWzB()KUBP`Iy}_wI|DdoLh&}nR_Ty_CmYUb?&y6X`}-l6{FIF-rz~ZqRsn?Fw7M6 z-s3!^UgF#^jS?}cZNtNZhZk(l7^)|> z!qcKGO?yQ4BrqY??krFhu?GpIs0P)*?Xy;mRLZ}q1xB&A6f76{QuVbY&cE_{cMSDS zEXK(`/libraries/${encodeURIComponent(id)}/count`, { + ...opts + })); +} export function scanLibrary({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -2107,16 +2111,6 @@ export function scanLibrary({ id }: { method: "POST" })); } -export function getLibraryStatistics({ id }: { - id: string; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: LibraryStatsResponseDto; - }>(`/libraries/${encodeURIComponent(id)}/statistics`, { - ...opts - })); -} export function validate({ id, validateLibraryDto }: { id: string; validateLibraryDto: ValidateLibraryDto; diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index b8959ca288..adf0f6c106 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -57,10 +57,10 @@ export class LibraryController { return this.service.validate(id, dto); } - @Get(':id/statistics') + @Get(':id/count') @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) - getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { - return this.service.getStatistics(id); + getAssetCount(@Param() { id }: UUIDParamDto): Promise { + return this.service.getAssetCount(id); } @Post(':id/scan') diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index b388a23392..f9e9a4dd21 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -201,5 +201,5 @@ export interface IAssetRepository { upsertFiles(files: UpsertFileOptions[]): Promise; updateOffline(library: LibraryEntity): Promise; getNewPaths(libraryId: string, paths: string[]): Promise; - getAssetCount(id: string, options: AssetSearchOptions): Promise; + getAssetCount(options: AssetSearchOptions): Promise; } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 6f8d81408e..cc01d0c9be 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -786,7 +786,7 @@ export class AssetRepository implements IAssetRepository { .then((result) => result.map((row: { path: string }) => row.path)); } - async getAssetCount(id: string, options: AssetSearchOptions = {}): Promise { + async getAssetCount(options: AssetSearchOptions = {}): Promise { let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files'); builder = searchAssetBuilder(builder, options); return builder.getCount(); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 8751037119..f2bc09c907 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -249,7 +249,12 @@ export class AssetService extends BaseService { const { thumbnailFile, previewFile } = getAssetFiles(asset.files); const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath]; - if (deleteOnDisk) { + + if (deleteOnDisk && !asset.isOffline) { + /* We don't want to delete an offline asset because it is either... + ...missing from disk => don't delete the file since it doesn't exist where we expect + ...outside of any import path => don't delete the file since we're not responsible for it + ...matching an exclusion pattern => don't delete the file since it's excluded */ files.push(asset.sidecarPath, asset.originalPath); } diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 2faed0a516..a9a430858e 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -266,7 +266,7 @@ export class JobService extends BaseService { } case JobName.GENERATE_THUMBNAILS: { - if (!item.data.notify && item.data.source !== 'upload') { + if (!item.data.notify && item.data.source !== 'upload' && item.data.source !== 'library-import') { break; } diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 7fbaa40f6f..3c4e7f7a28 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { R_OK } from 'node:constants'; import path, { basename, isAbsolute, parse } from 'node:path'; import picomatch from 'picomatch'; @@ -174,12 +174,12 @@ export class LibraryService extends BaseService { } } - async getStatistics(id: string): Promise { - const statistics = await this.libraryRepository.getStatistics(id); - if (!statistics) { - throw new BadRequestException(`Library ${id} not found`); + async getAssetCount(id: string): Promise { + const count = await this.assetRepository.getAssetCount({ libraryId: id }); + if (count == undefined) { + throw new InternalServerErrorException(`Failed to get asset count for library ${id}`); } - return statistics; + return count; } async get(id: string): Promise { @@ -354,7 +354,8 @@ export class LibraryService extends BaseService { private processEntity(filePath: string, ownerId: string, libraryId: string): AssetCreate { const assetPath = path.normalize(filePath); - const now = new Date(); + // This date will be set until metadata extraction runs + const datePlaceholder = new Date('1900-01-01'); return { ownerId: ownerId, @@ -365,9 +366,9 @@ export class LibraryService extends BaseService { // TODO: device asset id is deprecated, remove it deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''), deviceId: 'Library Import', - fileCreatedAt: now, - fileModifiedAt: now, - localDateTime: now, + fileCreatedAt: datePlaceholder, + fileModifiedAt: datePlaceholder, + localDateTime: datePlaceholder, type: mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE, originalFileName: parse(assetPath).base, isExternal: true, @@ -620,7 +621,7 @@ export class LibraryService extends BaseService { return JobStatus.SKIPPED; } - const assetCount = await this.assetRepository.getAssetCount(library.id, { withDeleted: true }); + const assetCount = await this.assetRepository.getAssetCount({ libraryId: job.id, withDeleted: true }); if (!assetCount) { this.logger.log(`Library ${library.id} is empty, no need to check assets`); diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 621dee0f81..549963772d 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -52,7 +52,7 @@ export class TrashService extends BaseService { ); for await (const assetIds of assetPagination) { - this.logger.debug(`Queueing ${assetIds.length} assets for deletion from the trash`); + this.logger.debug(`Queueing ${assetIds.length} asset(s) for deletion from the trash`); count += assetIds.length; await this.jobRepository.queueAll( assetIds.map((assetId) => ({ diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index b89e81ebf6..20d35ff76d 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -12,18 +12,16 @@ notificationController, NotificationType, } from '$lib/components/shared-components/notification/notification'; - import { ByteUnit, getBytesWithUnit } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { createLibrary, deleteLibrary, getAllLibraries, - getLibraryStatistics, + getAssetCount, getUserAdmin, scanLibrary, updateLibrary, type LibraryResponseDto, - type LibraryStatsResponseDto, type UserResponseDto, } from '@immich/sdk'; import { mdiDatabase, mdiDotsVertical, mdiPlusBoxOutline, mdiSync } from '@mdi/js'; @@ -44,13 +42,8 @@ let libraries: LibraryResponseDto[] = $state([]); - let stats: LibraryStatsResponseDto[] = []; let owner: UserResponseDto[] = $state([]); - let photos: number[] = []; - let videos: number[] = []; - let totalCount: number[] = $state([]); - let diskUsage: number[] = $state([]); - let diskUsageUnit: ByteUnit[] = $state([]); + let assetCount: number[] = $state([]); let editImportPaths: number | undefined = $state(); let editScanSettings: number | undefined = $state(); let renameLibrary: number | undefined = $state(); @@ -74,12 +67,8 @@ }; const refreshStats = async (listIndex: number) => { - stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id }); + assetCount[listIndex] = await getAssetCount({ id: libraries[listIndex].id }); owner[listIndex] = await getUserAdmin({ id: libraries[listIndex].ownerId }); - photos[listIndex] = stats[listIndex].photos; - videos[listIndex] = stats[listIndex].videos; - totalCount[listIndex] = stats[listIndex].total; - [diskUsage[listIndex], diskUsageUnit[listIndex]] = getBytesWithUnit(stats[listIndex].usage, 0); }; async function readLibraryList() { @@ -190,10 +179,10 @@ } await refreshStats(index); - const assetCount = totalCount[index]; - if (assetCount > 0) { + const count = assetCount[index]; + if (count > 0) { const isConfirmed = await dialogController.show({ - prompt: $t('admin.confirm_delete_library_assets', { values: { count: assetCount } }), + prompt: $t('admin.confirm_delete_library_assets', { values: { count } }), }); if (!isConfirmed) { @@ -242,19 +231,18 @@ - + {$t('type')} {$t('name')} {$t('owner')} {$t('assets')} - {$t('size')} {#each libraries as library, index (library.id)} - {#if totalCount[index] == undefined} + {#if assetCount[index] == undefined} {:else} - {totalCount[index].toLocaleString($locale)} - {/if} - - - {#if diskUsage[index] == undefined} - - {:else} - {diskUsage[index]} - {diskUsageUnit[index]} + {assetCount[index].toLocaleString($locale)} {/if}