diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 8190f94c6ab5c..2a207d52a9865 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -337,6 +337,7 @@ Class | Method | HTTP request | Description - [JobSettingsDto](doc//JobSettingsDto.md) - [JobStatusDto](doc//JobStatusDto.md) - [LibraryResponseDto](doc//LibraryResponseDto.md) + - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LicenseKeyDto](doc//LicenseKeyDto.md) - [LicenseResponseDto](doc//LicenseResponseDto.md) - [LogLevel](doc//LogLevel.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3fccede06eb50..73eb02d89ed7a 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -150,6 +150,7 @@ part 'model/job_name.dart'; part 'model/job_settings_dto.dart'; part 'model/job_status_dto.dart'; part 'model/library_response_dto.dart'; +part 'model/library_stats_response_dto.dart'; part 'model/license_key_dto.dart'; part 'model/license_response_dto.dart'; part 'model/log_level.dart'; diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart index 9ed89fcff24fa..36d98d9a88a78 100644 --- a/mobile/openapi/lib/api/libraries_api.dart +++ b/mobile/openapi/lib/api/libraries_api.dart @@ -228,7 +228,7 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future getLibraryStatistics(String id,) async { + Future getLibraryStatistics(String id,) async { final response = await getLibraryStatisticsWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -237,7 +237,7 @@ class LibrariesApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'num',) as num; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LibraryStatsResponseDto',) as LibraryStatsResponseDto; } return null; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index aa5db6589b462..a6f8d551da81c 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -354,6 +354,8 @@ class ApiClient { return JobStatusDto.fromJson(value); case 'LibraryResponseDto': return LibraryResponseDto.fromJson(value); + case 'LibraryStatsResponseDto': + return LibraryStatsResponseDto.fromJson(value); case 'LicenseKeyDto': return LicenseKeyDto.fromJson(value); case 'LicenseResponseDto': diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart new file mode 100644 index 0000000000000..afe67da31a251 --- /dev/null +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -0,0 +1,123 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class LibraryStatsResponseDto { + /// Returns a new [LibraryStatsResponseDto] instance. + LibraryStatsResponseDto({ + this.photos = 0, + this.total = 0, + this.usage = 0, + this.videos = 0, + }); + + int photos; + + int total; + + int usage; + + int videos; + + @override + bool operator ==(Object other) => identical(this, other) || other is LibraryStatsResponseDto && + other.photos == photos && + other.total == total && + other.usage == usage && + other.videos == videos; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (photos.hashCode) + + (total.hashCode) + + (usage.hashCode) + + (videos.hashCode); + + @override + String toString() => 'LibraryStatsResponseDto[photos=$photos, total=$total, usage=$usage, videos=$videos]'; + + Map toJson() { + final json = {}; + json[r'photos'] = this.photos; + json[r'total'] = this.total; + json[r'usage'] = this.usage; + json[r'videos'] = this.videos; + return json; + } + + /// Returns a new [LibraryStatsResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static LibraryStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LibraryStatsResponseDto"); + if (value is Map) { + final json = value.cast(); + + return LibraryStatsResponseDto( + photos: mapValueOfType(json, r'photos')!, + total: mapValueOfType(json, r'total')!, + usage: mapValueOfType(json, r'usage')!, + videos: mapValueOfType(json, r'videos')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = LibraryStatsResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = LibraryStatsResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of LibraryStatsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = LibraryStatsResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'photos', + 'total', + 'usage', + 'videos', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 01de992d8d3c7..505a9e93f096d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2907,7 +2907,7 @@ "content": { "application/json": { "schema": { - "type": "number" + "$ref": "#/components/schemas/LibraryStatsResponseDto" } } }, @@ -9540,6 +9540,34 @@ ], "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 42aafac9ceb0c..80c1a667a3731 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -574,6 +574,12 @@ 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[]; @@ -2106,7 +2112,7 @@ export function getLibraryStatistics({ id }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: number; + data: LibraryStatsResponseDto; }>(`/libraries/${encodeURIComponent(id)}/statistics`, { ...opts })); diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index 14711b2db4bdb..b8959ca28875c 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { CreateLibraryDto, LibraryResponseDto, + LibraryStatsResponseDto, UpdateLibraryDto, ValidateLibraryDto, ValidateLibraryResponseDto, @@ -58,7 +59,7 @@ export class LibraryController { @Get(':id/statistics') @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) - getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { + getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(id); } diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 2502cb6f4b3a3..9ec8c968250a2 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,6 @@ import { Insertable, Updateable, UpdateResult } from 'kysely'; import { AssetJobStatus, Assets, Exif } from 'src/db'; import { AssetEntity } from 'src/entities/asset.entity'; -import { LibraryEntity } from 'src/entities/library.entity'; import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; @@ -171,7 +170,11 @@ export interface IAssetRepository { getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; upsertFile(file: UpsertFileOptions): Promise; upsertFiles(files: UpsertFileOptions[]): Promise; - detectOfflineExternalAssets(library: LibraryEntity): Promise; + detectOfflineExternalAssets( + libraryId: string, + importPaths: string[], + exclusionPatterns: string[], + ): Promise; filterNewExternalAssetPaths(libraryId: string, paths: string[]): Promise; getLibraryAssetCount(options: AssetSearchOptions): Promise; } diff --git a/server/src/main.ts b/server/src/main.ts index 3097eee69bd74..95b35c6915aea 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -13,7 +13,7 @@ if (immichApp) { let apiProcess: ChildProcess | undefined; const onError = (name: string, error: Error) => { - console.error(`${name} worker error: ${error}`); + console.error(`${name} worker error: ${error}, stack: ${error.stack}`); }; const onExit = (name: string, exitCode: number | null) => { diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index d50069f0a9298..d5a203ead7960 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -262,8 +262,9 @@ with from "assets" where - "assets"."deletedAt" is null - and "assets"."isVisible" = $2 + "assets"."fileCreatedAt" <= $2 + and "assets"."deletedAt" is null + and "assets"."isVisible" = $3 ) select "timeBucket", @@ -283,9 +284,10 @@ from "assets" left join "exif" on "assets"."id" = "exif"."assetId" where - "assets"."deletedAt" is null - and "assets"."isVisible" = $1 - and date_trunc($2, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $3 + "assets"."fileCreatedAt" <= $1 + and "assets"."deletedAt" is null + and "assets"."isVisible" = $2 + and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4 order by "assets"."localDateTime" desc diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 347990f04c316..6a4c93f48e832 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -130,7 +130,7 @@ select from "libraries" inner join "assets" on "assets"."libraryId" = "libraries"."id" - inner join "exif" on "exif"."assetId" = "assets"."id" + left join "exif" on "exif"."assetId" = "assets"."id" where "libraries"."id" = $6 group by diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index b7c0d13aa73f4..f26992c5f776b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -22,7 +22,6 @@ import { withStack, withTags, } from 'src/entities/asset.entity'; -import { LibraryEntity } from 'src/entities/library.entity'; import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; import { AssetDeltaSyncOptions, @@ -50,6 +49,8 @@ import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database'; import { globToSqlPattern } from 'src/utils/misc'; import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination'; +const ASSET_CUTOFF_DATE = new Date('9000-01-01'); + @Injectable() export class AssetRepository implements IAssetRepository { constructor(@InjectKysely() private db: Kysely) {} @@ -527,6 +528,7 @@ export class AssetRepository implements IAssetRepository { return this.db .selectFrom('assets') .selectAll('assets') + .where('assets.fileCreatedAt', '<=', ASSET_CUTOFF_DATE) .$call(withExif) .where('ownerId', '=', anyUuid(userIds)) .where('isVisible', '=', true) @@ -543,6 +545,7 @@ export class AssetRepository implements IAssetRepository { .with('assets', (qb) => qb .selectFrom('assets') + .where('assets.fileCreatedAt', '<=', ASSET_CUTOFF_DATE) .select(truncatedDate(options.size).as('timeBucket')) .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED)) .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null) @@ -592,6 +595,7 @@ export class AssetRepository implements IAssetRepository { async getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise { return hasPeople(this.db, options.personId ? [options.personId] : undefined) .selectAll('assets') + .where('assets.fileCreatedAt', '<=', ASSET_CUTOFF_DATE) .$call(withExif) .$if(!!options.albumId, (qb) => withAlbums(qb, { albumId: options.albumId })) .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) @@ -748,9 +752,16 @@ export class AssetRepository implements IAssetRepository { .execute(); } - async detectOfflineExternalAssets(library: LibraryEntity): Promise { - const paths = library.importPaths.map((importPath) => `${importPath}%`); - const exclusions = library.exclusionPatterns.map((pattern) => globToSqlPattern(pattern)); + @GenerateSql({ + params: [{ libraryId: DummyValue.UUID, importPaths: [DummyValue.STRING], exclusionPatterns: [DummyValue.STRING] }], + }) + async detectOfflineExternalAssets( + libraryId: string, + importPaths: string[], + exclusionPatterns: string[], + ): Promise { + const paths = importPaths.map((importPath) => `${importPath}%`); + const exclusions = exclusionPatterns.map((pattern) => globToSqlPattern(pattern)); return this.db .updateTable('assets') @@ -760,13 +771,16 @@ export class AssetRepository implements IAssetRepository { }) .where('isOffline', '=', false) .where('isExternal', '=', true) - .where('libraryId', '=', asUuid(library.id)) + .where('libraryId', '=', asUuid(libraryId)) .where((eb) => eb.or([eb('originalPath', 'not like', paths.join('|')), eb('originalPath', 'like', exclusions.join('|'))]), ) .executeTakeFirstOrThrow(); } + @GenerateSql({ + params: [{ libraryId: DummyValue.UUID, paths: [DummyValue.STRING] }], + }) async filterNewExternalAssetPaths(libraryId: string, paths: string[]): Promise { const result = await this.db .selectFrom( diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index ca279d7dea3fe..dea1b1efec83a 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -100,7 +100,7 @@ export class LibraryRepository implements ILibraryRepository { const stats = await this.db .selectFrom('libraries') .innerJoin('assets', 'assets.libraryId', 'libraries.id') - .innerJoin('exif', 'exif.assetId', 'assets.id') + .leftJoin('exif', 'exif.assetId', 'assets.id') .select((eb) => eb.fn .count('assets.id') diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index c7a383b552721..f8257345b04da 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -231,7 +231,11 @@ describe(LibraryService.name, () => { const response = await sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id }); expect(response).toBe(JobStatus.SUCCESS); - expect(assetMock.detectOfflineExternalAssets).toHaveBeenCalledWith(libraryStub.externalLibrary1); + expect(assetMock.detectOfflineExternalAssets).toHaveBeenCalledWith( + libraryStub.externalLibrary1.id, + libraryStub.externalLibrary1.importPaths, + libraryStub.externalLibrary1.exclusionPatterns, + ); }); it('should skip an empty library', async () => { @@ -270,7 +274,11 @@ describe(LibraryService.name, () => { }); expect(response).toBe(JobStatus.SUCCESS); - expect(assetMock.detectOfflineExternalAssets).toHaveBeenCalledWith(libraryStub.externalLibraryWithImportPaths1); + expect(assetMock.detectOfflineExternalAssets).toHaveBeenCalledWith( + libraryStub.externalLibraryWithImportPaths1.id, + libraryStub.externalLibraryWithImportPaths1.importPaths, + libraryStub.externalLibraryWithImportPaths1.exclusionPatterns, + ); }); it("should fail if library can't be found", async () => { diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 1eac80913468f..bde8b03f3b8e8 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -7,6 +7,7 @@ import { OnEvent, OnJob } from 'src/decorators'; import { CreateLibraryDto, LibraryResponseDto, + LibraryStatsResponseDto, mapLibrary, UpdateLibraryDto, ValidateLibraryDto, @@ -24,6 +25,8 @@ import { mimeTypes } from 'src/utils/mime-types'; import { handlePromiseError } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; +const ASSET_IMPORT_DATE = new Date('9999-12-31'); + @Injectable() export class LibraryService extends BaseService { private watchLibraries = false; @@ -179,12 +182,12 @@ export class LibraryService extends BaseService { } } - async getStatistics(id: string): Promise { - const count = await this.assetRepository.getLibraryAssetCount({ libraryId: id }); - if (count == undefined) { - throw new InternalServerErrorException(`Failed to get asset count for library ${id}`); + async getStatistics(id: string): Promise { + const statistics = await this.libraryRepository.getStatistics(id); + if (!statistics) { + throw new BadRequestException(`Library ${id} not found`); } - return count; + return statistics; } async get(id: string): Promise { @@ -378,9 +381,9 @@ export class LibraryService extends BaseService { checksum: this.cryptoRepository.hashSha1(`path:${assetPath}`), originalPath: assetPath, - fileCreatedAt: new Date(), - fileModifiedAt: new Date(), - localDateTime: new Date(), + fileCreatedAt: ASSET_IMPORT_DATE, + fileModifiedAt: ASSET_IMPORT_DATE, + localDateTime: ASSET_IMPORT_DATE, // TODO: device asset id is deprecated, remove it deviceAssetId: `${basename(assetPath)}`.replaceAll(/\s+/g, ''), deviceId: 'Library Import', @@ -470,22 +473,25 @@ export class LibraryService extends BaseService { case AssetSyncResult.CHECK_OFFLINE: { const isInImportPath = job.importPaths.find((path) => asset.originalPath.startsWith(path)); - if (isInImportPath) { - const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern)); - - if (isExcluded) { - this.logger.verbose( - `Offline asset ${asset.originalPath} is in an import path but still covered by exclusion pattern, keeping offline in library ${job.libraryId}`, - ); - } else { - this.logger.debug(`Offline asset ${asset.originalPath} is now online in library ${job.libraryId}`); - assetIdsToOnline.push(asset.id); - } - } else { + if (!isInImportPath) { this.logger.verbose( `Offline asset ${asset.originalPath} is still not in any import path, keeping offline in library ${job.libraryId}`, ); + break; + } + + const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern)); + + if (!isExcluded) { + this.logger.debug(`Offline asset ${asset.originalPath} is now online in library ${job.libraryId}`); + assetIdsToOnline.push(asset.id); + break; } + + this.logger.verbose( + `Offline asset ${asset.originalPath} is in an import path but still covered by exclusion pattern, keeping offline in library ${job.libraryId}`, + ); + break; } } @@ -696,7 +702,11 @@ export class LibraryService extends BaseService { `Checking ${assetCount} asset(s) against import paths and exclusion patterns in library ${library.id}...`, ); - const offlineResult = await this.assetRepository.detectOfflineExternalAssets(library); + const offlineResult = await this.assetRepository.detectOfflineExternalAssets( + library.id, + library.importPaths, + library.exclusionPatterns, + ); const affectedAssetCount = Number(offlineResult.numUpdatedRows); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index f1d6389e02f52..4b092d27614e4 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -175,10 +175,10 @@ export class MetadataService extends BaseService { let fileCreatedAtDate = dateTimeOriginal; let fileModifiedAtDate = modifyDate; - /* if (asset.isExternal) { + if (asset.isExternal) { fileCreatedAtDate = fileCreatedAt; fileModifiedAtDate = fileModifiedAt; - } */ + } const exifData: Insertable = { assetId: asset.id, diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 01d73e418d06e..0ab49478ba6ba 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -43,5 +43,6 @@ export const newAssetRepositoryMock = (): Mocked => { upsertFiles: vitest.fn(), detectOfflineExternalAssets: vitest.fn(), filterNewExternalAssetPaths: vitest.fn(), + updateByLibraryId: vitest.fn(), }; }; diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 0c308dd387fd2..eb3f22878e623 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -15,6 +15,7 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import { locale } from '$lib/stores/preferences.store'; + import { ByteUnit, getBytesWithUnit } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; import { createLibrary, @@ -25,6 +26,7 @@ scanLibrary, updateLibrary, type LibraryResponseDto, + type LibraryStatsResponseDto, type UserResponseDto, } from '@immich/sdk'; import { Button } from '@immich/ui'; @@ -42,8 +44,13 @@ let libraries: LibraryResponseDto[] = $state([]); + let stats: LibraryStatsResponseDto[] = []; let owner: UserResponseDto[] = $state([]); - let assetCount: number[] = $state([]); + let photos: number[] = []; + let videos: number[] = []; + let totalCount: number[] = $state([]); + let diskUsage: number[] = $state([]); + let diskUsageUnit: ByteUnit[] = $state([]); let editImportPaths: number | undefined = $state(); let editScanSettings: number | undefined = $state(); let renameLibrary: number | undefined = $state(); @@ -67,8 +74,12 @@ }; const refreshStats = async (listIndex: number) => { - assetCount[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id }); + stats[listIndex] = await getLibraryStatistics({ 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() { @@ -179,10 +190,10 @@ } await refreshStats(index); - const count = assetCount[index]; - if (count > 0) { + const assetCount = totalCount[index]; + if (assetCount > 0) { const isConfirmed = await dialogController.show({ - prompt: $t('admin.confirm_delete_library_assets', { values: { count } }), + prompt: $t('admin.confirm_delete_library_assets', { values: { count: assetCount } }), }); if (!isConfirmed) { @@ -231,18 +242,19 @@ - + {$t('type')} {$t('name')} {$t('owner')} {$t('assets')} + {$t('size')} {#each libraries as library, index (library.id)} - {#if assetCount[index] == undefined} + {#if totalCount[index] == undefined} {:else} - {assetCount[index].toLocaleString($locale)} + {totalCount[index].toLocaleString($locale)} + {/if} + + + {#if diskUsage[index] == undefined} + + {:else} + {diskUsage[index]} + {diskUsageUnit[index]} {/if}