diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b336b1bfb6..e03f4dac77 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 73eb02d89e..3fccede06e 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart index 36d98d9a88..6010b7a9fc 100644 Binary files a/mobile/openapi/lib/api/libraries_api.dart and b/mobile/openapi/lib/api/libraries_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a6f8d551da..aa5db6589b 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart deleted file mode 100644 index afe67da31a..0000000000 Binary files a/mobile/openapi/lib/model/library_stats_response_dto.dart and /dev/null differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 7c8aba3b5e..554fed25d6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2853,9 +2853,9 @@ ] } }, - "/libraries/{id}/scan": { - "post": { - "operationId": "scanLibrary", + "/libraries/{id}/count": { + "get": { + "operationId": "getAssetCount", "parameters": [ { "name": "id", @@ -2868,7 +2868,14 @@ } ], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + }, "description": "" } }, @@ -2888,9 +2895,9 @@ ] } }, - "/libraries/{id}/statistics": { - "get": { - "operationId": "getLibraryStatistics", + "/libraries/{id}/scan": { + "post": { + "operationId": "scanLibrary", "parameters": [ { "name": "id", @@ -2903,14 +2910,7 @@ } ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LibraryStatsResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -9464,34 +9464,6 @@ ], "type": "object" }, - "LibraryStatsResponseDto": { - "properties": { - "photos": { - "default": 0, - "type": "integer" - }, - "total": { - "default": 0, - "type": "integer" - }, - "usage": { - "default": 0, - "format": "int64", - "type": "integer" - }, - "videos": { - "default": 0, - "type": "integer" - } - }, - "required": [ - "photos", - "total", - "usage", - "videos" - ], - "type": "object" - }, "LicenseKeyDto": { "properties": { "activationKey": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c31e71d05e..f441f47fc5 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -574,12 +574,6 @@ export type UpdateLibraryDto = { importPaths?: string[]; name?: string; }; -export type LibraryStatsResponseDto = { - photos: number; - total: number; - usage: number; - videos: number; -}; export type ValidateLibraryDto = { exclusionPatterns?: string[]; importPaths?: string[]; @@ -2099,6 +2093,16 @@ export function updateLibrary({ id, updateLibraryDto }: { body: updateLibraryDto }))); } +export function getAssetCount({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: number; + }>(`/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}