1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00
immich/server/src/repositories/asset.repository.ts

821 lines
24 KiB
TypeScript
Raw Normal View History

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DateTime } from 'luxon';
import path from 'node:path';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
2024-03-20 16:02:51 -05:00
import { AssetOrder } from 'src/entities/album.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from 'src/infra/infra.utils';
import { Instrumentation } from 'src/infra/instrumentation';
import {
2023-12-08 11:15:46 -05:00
AssetBuilderOptions,
AssetCreate,
2023-12-08 11:15:46 -05:00
AssetExploreFieldOptions,
AssetPathEntity,
AssetStats,
AssetStatsOptions,
AssetUpdateAllOptions,
AssetUpdateOptions,
IAssetRepository,
LivePhotoSearchOptions,
MapMarker,
MapMarkerSearchOptions,
2023-12-08 11:15:46 -05:00
MetadataSearchOptions,
MonthDay,
TimeBucketItem,
TimeBucketOptions,
TimeBucketSize,
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
WithProperty,
2023-12-08 11:15:46 -05:00
WithoutProperty,
} from 'src/interfaces/asset.repository';
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.repository';
import { Paginated, PaginationMode, PaginationOptions } from 'src/utils';
feat(server): separate face clustering job (#5598) * separate facial clustering job * update api * fixed some tests * invert clustering * hdbscan * update api * remove commented code * wip dbscan * cleanup removed cluster endpoint remove commented code * fixes updated tests minor fixes and formatting fixed queuing refinements * scale search range based on library size * defer non-core faces * optimizations removed unused query option * assign faces individually for correctness fixed unit tests remove unused method * don't select face embedding update sql linting fixed ml typing * updated job mock * paginate people query * select face embeddings because typeorm * fix setting face detection concurrency * update sql formatting linting * simplify logic remove unused imports * more specific delete signature * more accurate typing for face stubs * add migration formatting * chore: better typing * don't select embedding by default remove unused import * updated sql * use normal try/catch * stricter concurrency typing and enforcement * update api * update job concurrency panel to show disabled queues formatting * check jobId in queueAll fix tests * remove outdated comment * better facial recognition icon * wording wording formatting * fixed tests * fix * formatting & sql * try to fix sql check * more detailed description * update sql * formatting * wording * update `minFaces` description --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-01-18 00:08:48 -05:00
import {
Brackets,
FindOptionsRelations,
FindOptionsSelect,
FindOptionsWhere,
In,
IsNull,
Not,
Repository,
} from 'typeorm';
const truncateMap: Record<TimeBucketSize, string> = {
[TimeBucketSize.DAY]: 'day',
[TimeBucketSize.MONTH]: 'month',
};
2023-10-16 14:11:50 -04:00
const dateTrunc = (options: TimeBucketOptions) =>
`(date_trunc('${
truncateMap[options.size]
}', (asset."localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`;
@Instrumentation()
@Injectable()
export class AssetRepository implements IAssetRepository {
constructor(
@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
@InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
2023-12-08 11:15:46 -05:00
@InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository<SmartInfoEntity>,
) {}
async upsertExif(exif: Partial<ExifEntity>): Promise<void> {
await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] });
}
async upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void> {
await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] });
}
create(asset: AssetCreate): Promise<AssetEntity> {
return this.repository.save(asset);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] })
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
// For reference of a correct approach although slower
// let builder = this.repository
// .createQueryBuilder('asset')
// .leftJoin('asset.exifInfo', 'exifInfo')
// .where('asset.ownerId = :ownerId', { ownerId })
// .andWhere(
// `coalesce(date_trunc('day', asset."fileCreatedAt", "exifInfo"."timeZone") at TIME ZONE "exifInfo"."timeZone", date_trunc('day', asset."fileCreatedAt")) IN (:date)`,
// { date },
// )
// .andWhere('asset.isVisible = true')
// .andWhere('asset.isArchived = false')
// .orderBy('asset.fileCreatedAt', 'DESC');
// return builder.getMany();
return this.repository.find({
where: {
ownerId,
isVisible: true,
isArchived: false,
resizePath: Not(IsNull()),
fileCreatedAt: OptionalBetween(date, DateTime.fromJSDate(date).plus({ day: 1 }).toJSDate()),
},
relations: {
exifInfo: true,
},
order: {
fileCreatedAt: 'DESC',
},
});
}
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise<AssetEntity[]> {
return this.repository
.createQueryBuilder('entity')
.where(
`entity.ownerId IN (:...ownerIds)
AND entity.isVisible = true
AND entity.isArchived = false
AND entity.resizePath IS NOT NULL
AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day
AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`,
{
ownerIds,
day,
month,
},
)
.leftJoinAndSelect('entity.exifInfo', 'exifInfo')
.orderBy('entity.localDateTime', 'DESC')
.getMany();
}
@GenerateSql({ params: [[DummyValue.UUID]] })
@ChunkedArray()
feat(server): separate face clustering job (#5598) * separate facial clustering job * update api * fixed some tests * invert clustering * hdbscan * update api * remove commented code * wip dbscan * cleanup removed cluster endpoint remove commented code * fixes updated tests minor fixes and formatting fixed queuing refinements * scale search range based on library size * defer non-core faces * optimizations removed unused query option * assign faces individually for correctness fixed unit tests remove unused method * don't select face embedding update sql linting fixed ml typing * updated job mock * paginate people query * select face embeddings because typeorm * fix setting face detection concurrency * update sql formatting linting * simplify logic remove unused imports * more specific delete signature * more accurate typing for face stubs * add migration formatting * chore: better typing * don't select embedding by default remove unused import * updated sql * use normal try/catch * stricter concurrency typing and enforcement * update api * update job concurrency panel to show disabled queues formatting * check jobId in queueAll fix tests * remove outdated comment * better facial recognition icon * wording wording formatting * fixed tests * fix * formatting & sql * try to fix sql check * more detailed description * update sql * formatting * wording * update `minFaces` description --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-01-18 00:08:48 -05:00
getByIds(
ids: string[],
relations?: FindOptionsRelations<AssetEntity>,
select?: FindOptionsSelect<AssetEntity>,
): Promise<AssetEntity[]> {
return this.repository.find({
where: { id: In(ids) },
relations,
select,
withDeleted: true,
});
}
@GenerateSql({ params: [[DummyValue.UUID]] })
@ChunkedArray()
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]> {
return this.repository.find({
where: { id: In(ids) },
relations: {
exifInfo: true,
smartInfo: true,
tags: true,
2023-05-17 13:07:17 -04:00
faces: {
person: true,
},
stack: {
assets: true,
},
},
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
withDeleted: true,
});
}
@GenerateSql({ params: [DummyValue.UUID] })
async deleteAll(ownerId: string): Promise<void> {
await this.repository.delete({ ownerId });
}
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity> {
return paginate(this.repository, pagination, {
where: {
albums: {
id: albumId,
},
},
relations: {
albums: true,
exifInfo: true,
},
});
}
getByUserId(
pagination: PaginationOptions,
userId: string,
options: Omit<AssetSearchOptions, 'userIds'> = {},
): Paginated<AssetEntity> {
return this.getAll(pagination, { ...options, userIds: [userId] });
}
@GenerateSql({ params: [[DummyValue.UUID]] })
getLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
return paginate(this.repository, pagination, {
select: { id: true, originalPath: true, isOffline: true },
where: { library: { id: libraryId } },
});
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { library: { id: libraryId }, originalPath: originalPath },
});
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] })
@ChunkedArray({ paramIndex: 1 })
async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise<string[]> {
const result = await this.repository.query(
`
WITH paths AS (SELECT unnest($2::text[]) AS path)
SELECT path FROM paths
WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path);
`,
[libraryId, originalPaths],
);
return result.map((row: { path: string }) => row.path);
}
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] })
@ChunkedArray({ paramIndex: 1 })
async updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise<void> {
await this.repository.update(
{ library: { id: libraryId }, originalPath: Not(In(originalPaths)), isOffline: false },
{ isOffline: true },
);
}
getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
let builder = this.repository.createQueryBuilder('asset');
builder = searchAssetBuilder(builder, options);
builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC');
return paginatedBuilder<AssetEntity>(builder, {
mode: PaginationMode.SKIP_TAKE,
skip: pagination.skip,
take: pagination.take,
});
}
/**
* Get assets by device's Id on the database
* @param ownerId
* @param deviceId
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
const items = await this.repository.find({
select: { deviceAssetId: true },
where: {
ownerId,
deviceId,
isVisible: true,
},
withDeleted: true,
});
return items.map((asset) => asset.deviceAssetId);
}
@GenerateSql({ params: [DummyValue.UUID] })
2023-12-08 11:15:46 -05:00
getById(id: string, relations: FindOptionsRelations<AssetEntity>): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { id },
relations,
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
// We are specifically asking for this asset. Return it even if it is soft deleted
withDeleted: true,
});
}
@GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] })
@Chunked()
async updateAll(ids: string[], options: AssetUpdateAllOptions): Promise<void> {
await this.repository.update({ id: In(ids) }, options);
}
@Chunked()
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
async softDeleteAll(ids: string[]): Promise<void> {
await this.repository.softDelete({ id: In(ids), isExternal: false });
}
@Chunked()
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
async restoreAll(ids: string[]): Promise<void> {
await this.repository.restore({ id: In(ids) });
}
async update(asset: AssetUpdateOptions): Promise<void> {
await this.repository.update(asset.id, asset);
}
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
async remove(asset: AssetEntity): Promise<void> {
await this.repository.remove(asset);
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null> {
return this.repository.findOne({ where: { ownerId: userId, checksum } });
}
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null> {
const { ownerId, otherAssetId, livePhotoCID, type } = options;
return this.repository.findOne({
where: {
id: Not(otherAssetId),
ownerId,
type,
exifInfo: {
livePhotoCID,
},
},
relations: {
exifInfo: true,
},
});
}
@GenerateSql(
...Object.values(WithProperty)
.filter((property) => property !== WithProperty.IS_OFFLINE)
.map((property) => ({
name: property,
params: [DummyValue.PAGINATION, property],
})),
)
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
let relations: FindOptionsRelations<AssetEntity> = {};
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
switch (property) {
case WithoutProperty.THUMBNAIL: {
where = [
{ resizePath: IsNull(), isVisible: true },
{ resizePath: '', isVisible: true },
{ webpPath: IsNull(), isVisible: true },
{ webpPath: '', isVisible: true },
{ thumbhash: IsNull(), isVisible: true },
];
break;
}
case WithoutProperty.ENCODED_VIDEO: {
where = [
{ type: AssetType.VIDEO, encodedVideoPath: IsNull() },
{ type: AssetType.VIDEO, encodedVideoPath: '' },
];
break;
}
case WithoutProperty.EXIF: {
relations = {
exifInfo: true,
jobStatus: true,
};
where = {
isVisible: true,
jobStatus: {
metadataExtractedAt: IsNull(),
},
};
break;
}
case WithoutProperty.SMART_SEARCH: {
relations = {
2023-12-08 11:15:46 -05:00
smartSearch: true,
};
where = {
isVisible: true,
2023-05-31 11:00:37 -04:00
resizePath: Not(IsNull()),
2023-12-08 11:15:46 -05:00
smartSearch: {
embedding: IsNull(),
},
};
break;
}
case WithoutProperty.OBJECT_TAGS: {
relations = {
smartInfo: true,
};
where = {
2023-05-31 11:00:37 -04:00
resizePath: Not(IsNull()),
isVisible: true,
smartInfo: {
tags: IsNull(),
},
};
break;
}
case WithoutProperty.FACES: {
2023-05-17 13:07:17 -04:00
relations = {
faces: true,
jobStatus: true,
2023-05-17 13:07:17 -04:00
};
where = {
2023-05-30 14:51:53 -04:00
resizePath: Not(IsNull()),
2023-05-17 13:07:17 -04:00
isVisible: true,
faces: {
assetId: IsNull(),
personId: IsNull(),
},
jobStatus: {
facesRecognizedAt: IsNull(),
},
2023-05-17 13:07:17 -04:00
};
break;
}
2023-05-17 13:07:17 -04:00
case WithoutProperty.PERSON: {
feat(server): separate face clustering job (#5598) * separate facial clustering job * update api * fixed some tests * invert clustering * hdbscan * update api * remove commented code * wip dbscan * cleanup removed cluster endpoint remove commented code * fixes updated tests minor fixes and formatting fixed queuing refinements * scale search range based on library size * defer non-core faces * optimizations removed unused query option * assign faces individually for correctness fixed unit tests remove unused method * don't select face embedding update sql linting fixed ml typing * updated job mock * paginate people query * select face embeddings because typeorm * fix setting face detection concurrency * update sql formatting linting * simplify logic remove unused imports * more specific delete signature * more accurate typing for face stubs * add migration formatting * chore: better typing * don't select embedding by default remove unused import * updated sql * use normal try/catch * stricter concurrency typing and enforcement * update api * update job concurrency panel to show disabled queues formatting * check jobId in queueAll fix tests * remove outdated comment * better facial recognition icon * wording wording formatting * fixed tests * fix * formatting & sql * try to fix sql check * more detailed description * update sql * formatting * wording * update `minFaces` description --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-01-18 00:08:48 -05:00
relations = {
faces: true,
};
where = {
resizePath: Not(IsNull()),
isVisible: true,
faces: {
assetId: Not(IsNull()),
personId: IsNull(),
},
};
break;
}
feat(server): separate face clustering job (#5598) * separate facial clustering job * update api * fixed some tests * invert clustering * hdbscan * update api * remove commented code * wip dbscan * cleanup removed cluster endpoint remove commented code * fixes updated tests minor fixes and formatting fixed queuing refinements * scale search range based on library size * defer non-core faces * optimizations removed unused query option * assign faces individually for correctness fixed unit tests remove unused method * don't select face embedding update sql linting fixed ml typing * updated job mock * paginate people query * select face embeddings because typeorm * fix setting face detection concurrency * update sql formatting linting * simplify logic remove unused imports * more specific delete signature * more accurate typing for face stubs * add migration formatting * chore: better typing * don't select embedding by default remove unused import * updated sql * use normal try/catch * stricter concurrency typing and enforcement * update api * update job concurrency panel to show disabled queues formatting * check jobId in queueAll fix tests * remove outdated comment * better facial recognition icon * wording wording formatting * fixed tests * fix * formatting & sql * try to fix sql check * more detailed description * update sql * formatting * wording * update `minFaces` description --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-01-18 00:08:48 -05:00
case WithoutProperty.SIDECAR: {
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
where = [
{ sidecarPath: IsNull(), isVisible: true },
{ sidecarPath: '', isVisible: true },
];
break;
}
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
default: {
throw new Error(`Invalid getWithout property: ${property}`);
}
}
return paginate(this.repository, pagination, {
relations,
where,
order: {
// Ensures correct order when paginating
createdAt: 'ASC',
},
});
}
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity> {
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
switch (property) {
case WithProperty.SIDECAR: {
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
break;
}
case WithProperty.IS_OFFLINE: {
if (!libraryId) {
throw new Error('Library id is required when finding offline assets');
}
where = [{ isOffline: true, libraryId: libraryId }];
break;
}
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
default: {
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
throw new Error(`Invalid getWith property: ${property}`);
}
feat(server): xmp sidecar metadata (#2466) * initial commit for XMP sidecar support * Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards * didn't mean to commit default log level during testing * new sidecar logic for video metadata as well * Added xml mimetype for sidecars only * don't need capture group for this regex * wrong default value reverted * simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway * simplified setter logic Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * simplified logic per suggestions * sidecar is now its own queue with a discover and sync, updated UI for the new job queueing * queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar * now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync * simplified logic of filename extraction and asset instantiation * not sure how that got deleted.. * updated code per suggestions and comments in the PR * stat was not being used, removed the variable set * better type checking, using in-scope variables for exif getter instead of passing in every time * removed commented out test * ran and resolved all lints, formats, checks, and tests * resolved suggested change in PR * made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking * better error handling and moving files back to positions on move or save failure * regenerated api * format fixes * Added XMP documentation * documentation typo * Merged in main * missed merge conflict * more changes due to a merge * Resolving conflicts * added icon for sidecar jobs --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
}
return paginate(this.repository, pagination, {
where,
order: {
// Ensures correct order when paginating
createdAt: 'ASC',
},
});
}
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { albums: { id: albumId } },
order: { fileCreatedAt: 'DESC' },
});
}
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
return this.repository.findOne({
where: { albums: { id: albumId } },
order: { updatedAt: 'DESC' },
});
}
async getMapMarkers(ownerIds: string[], options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
const assets = await this.repository.find({
select: {
id: true,
exifInfo: {
city: true,
state: true,
country: true,
latitude: true,
longitude: true,
},
},
where: {
ownerId: In([...ownerIds]),
isVisible: true,
isArchived,
exifInfo: {
latitude: Not(IsNull()),
longitude: Not(IsNull()),
},
isFavorite,
fileCreatedAt: OptionalBetween(fileCreatedAfter, fileCreatedBefore),
},
relations: {
exifInfo: true,
},
order: {
fileCreatedAt: 'DESC',
},
});
return assets.map((asset) => ({
id: asset.id,
lat: asset.exifInfo!.latitude!,
lon: asset.exifInfo!.longitude!,
city: asset.exifInfo!.city,
state: asset.exifInfo!.state,
country: asset.exifInfo!.country,
}));
}
async getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats> {
let builder = this.repository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)`, 'count')
.addSelect(`asset.type`, 'type')
.where('"ownerId" = :ownerId', { ownerId })
.andWhere('asset.isVisible = true')
.groupBy('asset.type');
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
const { isArchived, isFavorite, isTrashed } = options;
if (isArchived !== undefined) {
builder = builder.andWhere(`asset.isArchived = :isArchived`, { isArchived });
}
if (isFavorite !== undefined) {
builder = builder.andWhere(`asset.isFavorite = :isFavorite`, { isFavorite });
}
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
if (isTrashed !== undefined) {
builder = builder.withDeleted().andWhere(`asset.deletedAt is not null`);
}
const items = await builder.getRawMany();
const result: AssetStats = {
[AssetType.AUDIO]: 0,
[AssetType.IMAGE]: 0,
[AssetType.VIDEO]: 0,
[AssetType.OTHER]: 0,
};
for (const item of items) {
result[item.type as AssetType] = Number(item.count) || 0;
}
return result;
}
getRandom(ownerId: string, count: number): Promise<AssetEntity[]> {
// can't use queryBuilder because of custom OFFSET clause
return this.repository.query(
`SELECT *
FROM assets
WHERE "ownerId" = $1
OFFSET FLOOR(RANDOM() * (SELECT GREATEST(COUNT(*) - $2, 0) FROM ASSETS WHERE "ownerId" = $1)) LIMIT $2`,
[ownerId, count],
);
}
2024-01-03 23:32:52 -05:00
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
2023-10-16 14:11:50 -04:00
const truncated = dateTrunc(options);
return this.getBuilder(options)
.select(`COUNT(asset.id)::int`, 'count')
2023-10-16 14:11:50 -04:00
.addSelect(truncated, 'timeBucket')
.groupBy(truncated)
.orderBy(truncated, options.order === AssetOrder.ASC ? 'ASC' : 'DESC')
.getRawMany();
}
2024-01-03 23:32:52 -05:00
@GenerateSql({ params: [DummyValue.TIME_BUCKET, { size: TimeBucketSize.MONTH }] })
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
2023-10-16 14:11:50 -04:00
const truncated = dateTrunc(options);
return (
this.getBuilder(options)
.andWhere(`${truncated} = :timeBucket`, { timeBucket: timeBucket.replace(/^[+-]/, '') })
// First sort by the day in localtime (put it in the right bucket)
2023-10-16 14:11:50 -04:00
.orderBy(truncated, 'DESC')
// and then sort by the actual time
.addOrderBy('asset.fileCreatedAt', options.order === AssetOrder.ASC ? 'ASC' : 'DESC')
.getMany()
);
}
2023-12-08 11:15:46 -05:00
@GenerateSql({ params: [DummyValue.UUID, { minAssetsPerField: 5, maxFields: 12 }] })
async getAssetIdByCity(
ownerId: string,
{ minAssetsPerField, maxFields }: AssetExploreFieldOptions,
): Promise<SearchExploreItem<string>> {
const cte = this.exifRepository
.createQueryBuilder('e')
.select('city')
.groupBy('city')
.having('count(city) >= :minAssetsPerField', { minAssetsPerField });
2023-12-08 11:15:46 -05:00
const items = await this.getBuilder({
userIds: [ownerId],
exifInfo: false,
assetType: AssetType.IMAGE,
isArchived: false,
})
.select('c.city', 'value')
.addSelect('asset.id', 'data')
.distinctOn(['c.city'])
.innerJoin('exif', 'e', 'asset.id = e."assetId"')
.addCommonTableExpression(cte, 'cities')
.innerJoin('cities', 'c', 'c.city = e.city')
.limit(maxFields)
.getRawMany();
return { fieldName: 'exifInfo.city', items };
}
@GenerateSql({ params: [DummyValue.UUID, { minAssetsPerField: 5, maxFields: 12 }] })
async getAssetIdByTag(
ownerId: string,
{ minAssetsPerField, maxFields }: AssetExploreFieldOptions,
): Promise<SearchExploreItem<string>> {
const cte = this.smartInfoRepository
.createQueryBuilder('si')
.select('unnest(tags)', 'tag')
.groupBy('tag')
.having('count(*) >= :minAssetsPerField', { minAssetsPerField });
2023-12-08 11:15:46 -05:00
const items = await this.getBuilder({
userIds: [ownerId],
exifInfo: false,
assetType: AssetType.IMAGE,
isArchived: false,
})
.select('unnest(si.tags)', 'value')
.addSelect('asset.id', 'data')
.distinctOn(['unnest(si.tags)'])
.innerJoin('smart_info', 'si', 'asset.id = si."assetId"')
.addCommonTableExpression(cte, 'random_tags')
.innerJoin('random_tags', 't', 'si.tags @> ARRAY[t.tag]')
.limit(maxFields)
.getRawMany();
return { fieldName: 'smartInfo.tags', items };
}
private getBuilder(options: AssetBuilderOptions) {
const { isArchived, isFavorite, isTrashed, albumId, personId, userIds, withStacked, exifInfo, assetType } = options;
let builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true');
2023-12-08 11:15:46 -05:00
if (assetType !== undefined) {
builder = builder.andWhere('asset.type = :assetType', { assetType });
}
let stackJoined = false;
2023-12-08 11:15:46 -05:00
if (exifInfo !== false) {
stackJoined = true;
builder = builder
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.leftJoinAndSelect('asset.stack', 'stack')
.leftJoinAndSelect('stack.assets', 'stackedAssets');
2023-12-08 11:15:46 -05:00
}
if (albumId) {
builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId });
}
if (userIds) {
builder = builder.andWhere('asset.ownerId IN (:...userIds )', { userIds });
}
if (isArchived !== undefined) {
builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived });
}
if (isFavorite !== undefined) {
builder = builder.andWhere('asset.isFavorite = :isFavorite', { isFavorite });
}
feat(server): trash asset (#4015) * refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 07:01:14 +00:00
if (isTrashed !== undefined) {
builder = builder.andWhere(`asset.deletedAt ${isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted();
}
if (personId !== undefined) {
builder = builder
.innerJoin('asset.faces', 'faces')
.innerJoin('faces.person', 'person')
.andWhere('person.id = :personId', { personId });
}
if (withStacked) {
if (!stackJoined) {
builder = builder.leftJoinAndSelect('asset.stack', 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets');
}
builder = builder.andWhere(
new Brackets((qb) => qb.where('stack.primaryAssetId = asset.id').orWhere('asset.stackId IS NULL')),
);
}
2023-10-22 02:38:07 +00:00
return builder;
}
2023-12-08 11:15:46 -05:00
@GenerateSql({ params: [DummyValue.STRING, [DummyValue.UUID], { numResults: 250 }] })
async searchMetadata(
query: string,
userIds: string[],
{ numResults }: MetadataSearchOptions,
): Promise<AssetEntity[]> {
const rows = await this.getBuilder({
userIds: userIds,
exifInfo: false,
isArchived: false,
})
.select('asset.*')
.addSelect('e.*')
2023-12-08 11:15:46 -05:00
.addSelect('COALESCE(si.tags, array[]::text[])', 'tags')
.addSelect('COALESCE(si.objects, array[]::text[])', 'objects')
.innerJoin('exif', 'e', 'asset."id" = e."assetId"')
.leftJoin('smart_info', 'si', 'si."assetId" = asset."id"')
.andWhere(
fix(server): add filename search (#6394) Fixes https://github.com/immich-app/immich/issues/5982. There are basically three options: 1. Search `originalFileName` by dropping a file extension from the query (if present). Lower fidelity but very easy - just a standard index & equality. 2. Search `originalPath` by adding an index on `reverse(originalPath)` and using `starts_with(reverse(query) + "/", reverse(originalPath)`. A weird index & query but high fidelity. 3. Add a new generated column called `originalFileNameWithExtension` or something. More storage, kinda jank. TBH, I think (1) is good enough and easy to make better in the future. For example, if I search "DSC_4242.jpg", I don't really think it matters if "DSC_4242.mov" also shows up. edit: There's a fourth approach that we discussed a bit in Discord and decided we could switch to it in the future: using a GIN. The minor issue is that Postgres doesn't tokenize paths in a useful (they're a single token and it won't match against partial components). We can solve that by tokenizing it ourselves. For example: ``` immich=# with vecs as (select to_tsvector('simple', array_to_string(string_to_array('upload/library/sushain/2015/2015-08-09/IMG_275.JPG', '/'), ' ')) as vec) select * from vecs where vec @@ phraseto_tsquery('simple', array_to_string(string_to_array('library/sushain', '/'), ' ')); vec ------------------------------------------------------------------------------- '-08':6 '-09':7 '2015':4,5 'img_275.jpg':8 'library':2 'sushain':3 'upload':1 (1 row) ``` The query is also tokenized with the 'split-by-slash-join-with-space' strategy. This strategy results in `IMG_275.JPG`, `2015`, `sushain` and `library/sushain` matching. But, `08` and `IMG_275` do not match. The former is because the token is `-08` and the latter because the `img_275.jpg` token is matched against exactly.
2024-01-15 12:40:28 -08:00
new Brackets((qb) => {
qb.where(
`(e."exifTextSearchableColumn" || COALESCE(si."smartInfoTextSearchableColumn", to_tsvector('english', '')))
@@ PLAINTO_TSQUERY('english', :query)`,
{ query },
).orWhere('asset."originalFileName" = :path', { path: path.parse(query).name });
}),
2023-12-08 11:15:46 -05:00
)
.addOrderBy('asset.fileCreatedAt', 'DESC')
2023-12-08 11:15:46 -05:00
.limit(numResults)
.getRawMany();
return rows.map(
({
tags,
objects,
country,
state,
city,
description,
model,
make,
dateTimeOriginal,
exifImageHeight,
exifImageWidth,
exposureTime,
fNumber,
fileSizeInByte,
focalLength,
iso,
latitude,
lensModel,
longitude,
modifyDate,
projectionType,
timeZone,
...assetInfo
}) =>
2023-12-08 11:15:46 -05:00
({
exifInfo: {
city,
country,
dateTimeOriginal,
2023-12-08 11:15:46 -05:00
description,
exifImageHeight,
exifImageWidth,
exposureTime,
fNumber,
fileSizeInByte,
focalLength,
iso,
latitude,
lensModel,
longitude,
2023-12-08 11:15:46 -05:00
make,
model,
modifyDate,
projectionType,
state,
timeZone,
2023-12-08 11:15:46 -05:00
},
smartInfo: {
tags,
objects,
},
...assetInfo,
}) as AssetEntity,
);
}
}