mirror of
https://github.com/immich-app/immich.git
synced 2025-02-03 01:22:44 +01:00
Merge b632b84fc3
into 060300de8a
This commit is contained in:
commit
85282a7328
32 changed files with 391 additions and 43 deletions
e2e/src/api/specs
server
src
db.d.ts
dtos
entities
interfaces
migrations
queries
access.repository.sqlactivity.repository.sqlalbum.repository.sqlasset.repository.sqlmemory.repository.sqlperson.repository.sqlsearch.repository.sqlshared.link.repository.sqlstack.repository.sql
repositories
access.repository.tsactivity.repository.tsalbum.repository.tsasset.repository.tsmemory.repository.tsperson.repository.tssearch.repository.tsshared-link.repository.tsstack.repository.ts
services
asset-media.service.spec.tslibrary.service.tsmetadata.service.tsstorage-template.service.spec.tsstorage-template.service.ts
utils
test/medium
|
@ -298,6 +298,7 @@ describe('/libraries', () => {
|
|||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||
|
|
30
server/src/db.d.ts
vendored
30
server/src/db.d.ts
vendored
|
@ -126,8 +126,8 @@ export interface Assets {
|
|||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
encodedVideoPath: Generated<string | null>;
|
||||
fileCreatedAt: Timestamp;
|
||||
fileModifiedAt: Timestamp;
|
||||
fileCreatedAt: Timestamp | null;
|
||||
fileModifiedAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
isArchived: Generated<boolean>;
|
||||
isExternal: Generated<boolean>;
|
||||
|
@ -136,7 +136,7 @@ export interface Assets {
|
|||
isVisible: Generated<boolean>;
|
||||
libraryId: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
localDateTime: Timestamp;
|
||||
localDateTime: Timestamp | null;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
ownerId: string;
|
||||
|
@ -214,6 +214,20 @@ export interface GeodataPlaces {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export interface GeodataPlacesTmp {
|
||||
admin1Code: string | null;
|
||||
admin1Name: string | null;
|
||||
admin2Code: string | null;
|
||||
admin2Name: string | null;
|
||||
alternateNames: string | null;
|
||||
countryCode: string;
|
||||
id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
modificationDate: Timestamp;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Libraries {
|
||||
createdAt: Generated<Timestamp>;
|
||||
deletedAt: Timestamp | null;
|
||||
|
@ -266,6 +280,14 @@ export interface NaturalearthCountries {
|
|||
type: string;
|
||||
}
|
||||
|
||||
export interface NaturalearthCountriesTmp {
|
||||
admin: string;
|
||||
admin_a3: string;
|
||||
coordinates: string;
|
||||
id: Generated<number>;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Partners {
|
||||
createdAt: Generated<Timestamp>;
|
||||
inTimeline: Generated<boolean>;
|
||||
|
@ -418,12 +440,14 @@ export interface DB {
|
|||
exif: Exif;
|
||||
face_search: FaceSearch;
|
||||
geodata_places: GeodataPlaces;
|
||||
geodata_places_tmp: GeodataPlacesTmp;
|
||||
libraries: Libraries;
|
||||
memories: Memories;
|
||||
memories_assets_assets: MemoriesAssetsAssets;
|
||||
migrations: Migrations;
|
||||
move_history: MoveHistory;
|
||||
naturalearth_countries: NaturalearthCountries;
|
||||
naturalearth_countries_tmp: NaturalearthCountriesTmp;
|
||||
partners: Partners;
|
||||
person: Person;
|
||||
sessions: Sessions;
|
||||
|
|
|
@ -113,6 +113,14 @@ const hexOrBufferToBase64 = (encoded: string | Buffer) => {
|
|||
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
const { stripMetadata = false, withStack = false } = options;
|
||||
|
||||
if (!entity.localDateTime) {
|
||||
throw new Error('Asset localDateTime is missing');
|
||||
} else if (!entity.fileCreatedAt) {
|
||||
throw new Error('Asset fileCreatedAt is missing');
|
||||
} else if (!entity.fileModifiedAt) {
|
||||
throw new Error('Asset fileModifiedAt is missing');
|
||||
}
|
||||
|
||||
if (stripMetadata) {
|
||||
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
||||
id: entity.id,
|
||||
|
|
|
@ -126,6 +126,13 @@ export class AssetStatsResponseDto {
|
|||
total!: number;
|
||||
}
|
||||
|
||||
export class AssetDatesDto {
|
||||
dateTimeOriginal!: Date;
|
||||
timeZone!: string | null;
|
||||
localDateTime!: Date;
|
||||
modifyDate!: Date;
|
||||
}
|
||||
|
||||
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
|
||||
return {
|
||||
images: stats[AssetType.IMAGE],
|
||||
|
|
|
@ -100,14 +100,14 @@ export class AssetEntity {
|
|||
deletedAt!: Date | null;
|
||||
|
||||
@Index('idx_asset_file_created_at')
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileCreatedAt!: Date;
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
fileCreatedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
localDateTime!: Date;
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
localDateTime!: Date | null;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
fileModifiedAt!: Date;
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
fileModifiedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isFavorite!: boolean;
|
||||
|
@ -399,5 +399,8 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
|||
)
|
||||
.$if(!!options.withExif, withExifInner)
|
||||
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null))
|
||||
.$if(!options.withNullFileModifiedAt, (qb) => qb.where('assets.fileModifiedAt', 'is not', null))
|
||||
.$if(!options.withNullFileCreatedAt, (qb) => qb.where('assets.fileCreatedAt', 'is not', null))
|
||||
.$if(!options.withNullLocalDateTime, (qb) => qb.where('assets.localDateTime', 'is not', null));
|
||||
}
|
||||
|
|
|
@ -63,6 +63,9 @@ export interface SearchStatusOptions {
|
|||
status?: AssetStatus;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
withNullLocalDateTime?: boolean;
|
||||
withNullFileCreatedAt?: boolean;
|
||||
withNullFileModifiedAt?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchOneToOneRelationOptions {
|
||||
|
|
18
server/src/migrations/1737845696644-NullableDates.ts
Normal file
18
server/src/migrations/1737845696644-NullableDates.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class NullableDates1737845696644 implements MigrationInterface {
|
||||
name = 'NullableDates1737845696644'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" SET NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -79,6 +79,9 @@ from
|
|||
inner join "albums_assets_assets" as "albumAssets" on "albums"."id" = "albumAssets"."albumsId"
|
||||
inner join "assets" on "assets"."id" = "albumAssets"."assetsId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
left join "albums_shared_users_users" as "albumUsers" on "albumUsers"."albumsId" = "albums"."id"
|
||||
left join "users" on "users"."id" = "albumUsers"."usersId"
|
||||
and "users"."deletedAt" is null
|
||||
|
@ -108,6 +111,9 @@ from
|
|||
and "sharedBy"."deletedAt" is null
|
||||
inner join "assets" on "assets"."ownerId" = "sharedBy"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
where
|
||||
"partner"."sharedWithId" = $1
|
||||
and "assets"."isArchived" = $2
|
||||
|
@ -126,6 +132,9 @@ from
|
|||
left join "shared_link__asset" on "shared_link__asset"."sharedLinksId" = "shared_links"."id"
|
||||
left join "assets" on "assets"."id" = "shared_link__asset"."assetsId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
left join "albums_assets_assets" on "albums_assets_assets"."albumsId" = "albums"."id"
|
||||
left join "assets" as "albumAssets" on "albumAssets"."id" = "albums_assets_assets"."assetsId"
|
||||
and "albumAssets"."deletedAt" is null
|
||||
|
@ -173,6 +182,9 @@ from
|
|||
"asset_faces"
|
||||
left join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
where
|
||||
"asset_faces"."id" in ($1)
|
||||
and "assets"."ownerId" = $2
|
||||
|
|
|
@ -25,6 +25,9 @@ from
|
|||
"activity"
|
||||
left join "assets" on "assets"."id" = "activity"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
where
|
||||
"activity"."albumId" = $1
|
||||
order by
|
||||
|
@ -43,3 +46,6 @@ where
|
|||
and "activity"."albumId" = $2
|
||||
and "activity"."isLiked" = $3
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
|
|
|
@ -98,6 +98,9 @@ select
|
|||
where
|
||||
"albums_assets_assets"."albumsId" = "albums"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"assets"."fileCreatedAt" desc
|
||||
) as "asset"
|
||||
|
@ -212,6 +215,9 @@ from
|
|||
where
|
||||
"albums"."id" in ($1)
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
group by
|
||||
"albums"."id"
|
||||
|
||||
|
|
|
@ -47,6 +47,9 @@ with
|
|||
and "asset_files"."type" = $6
|
||||
)
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
limit
|
||||
$7
|
||||
) as "a" on true
|
||||
|
@ -157,6 +160,8 @@ where
|
|||
"ownerId" = $1::uuid
|
||||
and "deviceId" = $2
|
||||
and "isVisible" = $3
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "deletedAt" is null
|
||||
|
||||
-- AssetRepository.getLivePhotoCount
|
||||
|
@ -239,6 +244,8 @@ where
|
|||
"assets"."sidecarPath" = $1
|
||||
or "assets"."sidecarPath" is null
|
||||
)
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."isVisible" = $2
|
||||
and "deletedAt" is null
|
||||
order by
|
||||
|
@ -258,6 +265,9 @@ with
|
|||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
)
|
||||
select
|
||||
"timeBucket",
|
||||
|
@ -298,6 +308,9 @@ where
|
|||
)
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
|
||||
order by
|
||||
"assets"."localDateTime" desc
|
||||
|
@ -324,6 +337,9 @@ with
|
|||
and "assets"."duplicateId" is not null
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
group by
|
||||
"assets"."duplicateId"
|
||||
),
|
||||
|
@ -381,6 +397,8 @@ from
|
|||
where
|
||||
"ownerId" = $2::uuid
|
||||
and "isVisible" = $3
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "isArchived" = $4
|
||||
and "type" = $5
|
||||
and "deletedAt" is null
|
||||
|
@ -410,6 +428,8 @@ from
|
|||
where
|
||||
"assets"."ownerId" = $1::uuid
|
||||
and "isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "updatedAt" <= $3
|
||||
and "assets"."id" > $4
|
||||
order by
|
||||
|
@ -440,6 +460,8 @@ from
|
|||
where
|
||||
"assets"."ownerId" = any ($1::uuid[])
|
||||
and "isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "updatedAt" > $3
|
||||
limit
|
||||
$4
|
||||
|
|
|
@ -26,6 +26,9 @@ select
|
|||
where
|
||||
"memories_assets_assets"."memoriesId" = "memories"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
|
@ -56,6 +59,9 @@ select
|
|||
where
|
||||
"memories_assets_assets"."memoriesId" = "memories"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
|
|
|
@ -169,6 +169,9 @@ from
|
|||
and "asset_faces"."personId" = $1
|
||||
and "assets"."isArchived" = $2
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."livePhotoVideoId" is null
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
|
@ -183,6 +186,9 @@ from
|
|||
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
|
||||
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."isArchived" = $2
|
||||
where
|
||||
"person"."ownerId" = $3
|
||||
|
|
|
@ -13,6 +13,9 @@ where
|
|||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"assets"."fileCreatedAt" desc
|
||||
limit
|
||||
|
@ -34,6 +37,9 @@ offset
|
|||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."id" < $6
|
||||
order by
|
||||
random()
|
||||
|
@ -54,6 +60,9 @@ union all
|
|||
and "assets"."isFavorite" = $11
|
||||
and "assets"."isArchived" = $12
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."id" > $13
|
||||
order by
|
||||
random()
|
||||
|
@ -77,6 +86,9 @@ where
|
|||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
smart_search.embedding <=> $6
|
||||
limit
|
||||
|
@ -98,6 +110,9 @@ with
|
|||
"assets"."ownerId" = any ($2::uuid[])
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $3
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."type" = $4
|
||||
and "assets"."id" != $5::uuid
|
||||
order by
|
||||
|
@ -126,6 +141,9 @@ with
|
|||
where
|
||||
"assets"."ownerId" = any ($2::uuid[])
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
face_search.embedding <=> $3
|
||||
limit
|
||||
|
@ -178,6 +196,9 @@ with recursive
|
|||
and "assets"."isArchived" = $3
|
||||
and "assets"."type" = $4
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"city"
|
||||
limit
|
||||
|
@ -203,6 +224,9 @@ with recursive
|
|||
and "assets"."isArchived" = $8
|
||||
and "assets"."type" = $9
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "exif"."city" > "cte"."city"
|
||||
order by
|
||||
"city"
|
||||
|
|
|
@ -31,6 +31,9 @@ from
|
|||
where
|
||||
"shared_links"."id" = "shared_link__asset"."sharedLinksId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"assets"."fileCreatedAt" asc
|
||||
) as "a" on true
|
||||
|
@ -65,6 +68,9 @@ from
|
|||
where
|
||||
"albums_assets_assets"."assetsId" = "assets"."id"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"assets"."fileCreatedAt" asc
|
||||
) as "assets" on true
|
||||
|
@ -112,6 +118,9 @@ from
|
|||
where
|
||||
"assets"."id" = "shared_link__asset"."assetsId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
) as "assets" on true
|
||||
left join lateral (
|
||||
select
|
||||
|
|
|
@ -23,6 +23,9 @@ select
|
|||
) as "exifInfo" on true
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
|
@ -68,6 +71,9 @@ select
|
|||
) as "exifInfo" on true
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
|
@ -113,6 +119,9 @@ select
|
|||
) as "exifInfo" on true
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
|
|
|
@ -138,7 +138,12 @@ class AssetAccess {
|
|||
.selectFrom('albums')
|
||||
.innerJoin('albums_assets_assets as albumAssets', 'albums.id', 'albumAssets.albumsId')
|
||||
.innerJoin('assets', (join) =>
|
||||
join.onRef('assets.id', '=', 'albumAssets.assetsId').on('assets.deletedAt', 'is', null),
|
||||
join
|
||||
.onRef('assets.id', '=', 'albumAssets.assetsId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id')
|
||||
.leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null))
|
||||
|
@ -194,7 +199,12 @@ class AssetAccess {
|
|||
join.onRef('sharedBy.id', '=', 'partner.sharedById').on('sharedBy.deletedAt', 'is', null),
|
||||
)
|
||||
.innerJoin('assets', (join) =>
|
||||
join.onRef('assets.ownerId', '=', 'sharedBy.id').on('assets.deletedAt', 'is', null),
|
||||
join
|
||||
.onRef('assets.ownerId', '=', 'sharedBy.id')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.select('assets.id')
|
||||
.where('partner.sharedWithId', '=', userId)
|
||||
|
@ -218,7 +228,12 @@ class AssetAccess {
|
|||
)
|
||||
.leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id')
|
||||
.leftJoin('assets', (join) =>
|
||||
join.onRef('assets.id', '=', 'shared_link__asset.assetsId').on('assets.deletedAt', 'is', null),
|
||||
join
|
||||
.onRef('assets.id', '=', 'shared_link__asset.assetsId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.leftJoin('albums_assets_assets', 'albums_assets_assets.albumsId', 'albums.id')
|
||||
.leftJoin('assets as albumAssets', (join) =>
|
||||
|
@ -369,7 +384,12 @@ class PersonAccess {
|
|||
.selectFrom('asset_faces')
|
||||
.select('asset_faces.id')
|
||||
.leftJoin('assets', (join) =>
|
||||
join.onRef('assets.id', '=', 'asset_faces.assetId').on('assets.deletedAt', 'is', null),
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.where('asset_faces.id', 'in', [...assetFaceIds])
|
||||
.where('assets.ownerId', '=', userId)
|
||||
|
|
|
@ -36,7 +36,14 @@ export class ActivityRepository {
|
|||
.selectFrom('activity')
|
||||
.selectAll('activity')
|
||||
.select(withUser)
|
||||
.leftJoin('assets', (join) => join.onRef('assets.id', '=', 'activity.assetId').on('assets.deletedAt', 'is', null))
|
||||
.leftJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('assets.id', '=', 'activity.assetId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.$if(!!userId, (qb) => qb.where('activity.userId', '=', userId!))
|
||||
.$if(assetId === null, (qb) => qb.where('assetId', 'is', null))
|
||||
.$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!))
|
||||
|
@ -65,6 +72,9 @@ export class ActivityRepository {
|
|||
.where('activity.albumId', '=', albumId)
|
||||
.where('activity.isLiked', '=', false)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return count as number;
|
||||
|
|
|
@ -63,6 +63,9 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
|||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.as('asset'),
|
||||
)
|
||||
|
@ -132,6 +135,9 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
.select((eb) => sql<number>`${eb.fn.count('assets.id')}::int`.as('assetCount'))
|
||||
.where('albums.id', 'in', ids)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.groupBy('albums.id')
|
||||
.execute();
|
||||
}
|
||||
|
@ -371,7 +377,12 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
return eb
|
||||
.selectFrom('albums_assets_assets as album_assets')
|
||||
.innerJoin('assets', (join) =>
|
||||
join.onRef('album_assets.assetsId', '=', 'assets.id').on('assets.deletedAt', 'is', null),
|
||||
join
|
||||
.onRef('album_assets.assetsId', '=', 'assets.id')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.whereRef('album_assets.albumsId', '=', 'albums.id');
|
||||
}
|
||||
|
|
|
@ -122,6 +122,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
),
|
||||
)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.limit(20)
|
||||
.as('a'),
|
||||
(join) => join.onTrue(),
|
||||
|
@ -291,6 +294,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('deviceId', '=', deviceId)
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('deletedAt', 'is', null)
|
||||
.execute();
|
||||
|
||||
|
@ -458,7 +463,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
.where('job_status.duplicatesDetectedAt', 'is', null)
|
||||
.where('job_status.previewAt', 'is not', null)
|
||||
.where((eb) => eb.exists(eb.selectFrom('smart_search').where('assetId', '=', eb.ref('assets.id'))))
|
||||
.where('assets.isVisible', '=', true),
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null),
|
||||
)
|
||||
.$if(property === WithoutProperty.ENCODED_VIDEO, (qb) =>
|
||||
qb
|
||||
|
@ -469,6 +476,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
qb
|
||||
.innerJoin('asset_job_status as job_status', 'assets.id', 'job_status.assetId')
|
||||
.where('job_status.metadataExtractedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.isVisible', '=', true),
|
||||
)
|
||||
.$if(property === WithoutProperty.FACES, (qb) =>
|
||||
|
@ -476,17 +485,23 @@ export class AssetRepository implements IAssetRepository {
|
|||
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
|
||||
.where('job_status.previewAt', 'is not', null)
|
||||
.where('job_status.facesRecognizedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.isVisible', '=', true),
|
||||
)
|
||||
.$if(property === WithoutProperty.SIDECAR, (qb) =>
|
||||
qb
|
||||
.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)]))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.isVisible', '=', true),
|
||||
)
|
||||
.$if(property === WithoutProperty.SMART_SEARCH, (qb) =>
|
||||
qb
|
||||
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
|
||||
.where('job_status.previewAt', 'is not', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where((eb) =>
|
||||
eb.not((eb) => eb.exists(eb.selectFrom('smart_search').whereRef('assetId', '=', 'assets.id'))),
|
||||
|
@ -495,6 +510,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(property === WithoutProperty.THUMBNAIL, (qb) =>
|
||||
qb
|
||||
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
|
@ -534,6 +551,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.where('ownerId', '=', anyUuid(ownerIds))
|
||||
.where('latitude', 'is not', null)
|
||||
.where('longitude', 'is not', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('isVisible', '=', true)
|
||||
.where('deletedAt', 'is', null)
|
||||
.$if(!!isArchived, (qb) => qb.where('isArchived', '=', isArchived!))
|
||||
|
@ -552,6 +571,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('isVisible', '=', true)
|
||||
.$if(isArchived !== undefined, (qb) => qb.where('isArchived', '=', isArchived!))
|
||||
.$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
|
||||
|
@ -567,6 +588,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$call(withExif)
|
||||
.where('ownerId', '=', anyUuid(userIds))
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy((eb) => eb.fn('random'))
|
||||
.limit(take)
|
||||
|
@ -584,6 +607,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb
|
||||
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
||||
|
@ -664,6 +690,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
|
||||
.orderBy('assets.localDateTime', options.order ?? 'desc')
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
|
@ -692,6 +721,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
.where('assets.duplicateId', 'is not', null)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.groupBy('assets.duplicateId'),
|
||||
)
|
||||
.with('unique', (qb) =>
|
||||
|
@ -738,6 +770,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.select(['assetId as data', 'exif.city as value'])
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('isArchived', '=', false)
|
||||
.where('type', '=', AssetType.IMAGE)
|
||||
.where('deletedAt', 'is', null)
|
||||
|
@ -778,6 +812,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.where('assets.ownerId', '=', asUuid(ownerId))
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('updatedAt', '<=', updatedUntil)
|
||||
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
|
||||
.orderBy('assets.id')
|
||||
|
@ -806,6 +842,8 @@ export class AssetRepository implements IAssetRepository {
|
|||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('updatedAt', '>', options.updatedAfter)
|
||||
.limit(options.limit)
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
|
|
|
@ -105,7 +105,10 @@ export class MemoryRepository implements IBulkAsset {
|
|||
.selectAll('assets')
|
||||
.innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId')
|
||||
.whereRef('memories_assets_assets.memoriesId', '=', 'memories.id')
|
||||
.where('assets.deletedAt', 'is', null),
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null),
|
||||
).as('assets'),
|
||||
)
|
||||
.where('id', '=', id)
|
||||
|
|
|
@ -128,7 +128,10 @@ export class PersonRepository implements IPersonRepository {
|
|||
join
|
||||
.onRef('asset_faces.assetId', '=', 'assets.id')
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null),
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.orderBy('person.isHidden', 'asc')
|
||||
|
@ -284,6 +287,9 @@ export class PersonRepository implements IPersonRepository {
|
|||
.on('asset_faces.personId', '=', personId)
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null)
|
||||
.on('assets.livePhotoVideoId', 'is', null),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
||||
|
@ -304,6 +310,9 @@ export class PersonRepository implements IPersonRepository {
|
|||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.fileCreatedAt', 'is not', null)
|
||||
.on('assets.fileModifiedAt', 'is not', null)
|
||||
.on('assets.localDateTime', 'is not', null)
|
||||
.on('assets.isArchived', '=', false),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
|
||||
|
|
|
@ -139,6 +139,9 @@ export class SearchRepository implements ISearchRepository {
|
|||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.where('assets.type', '=', type)
|
||||
.where('assets.id', '!=', asUuid(assetId))
|
||||
.orderBy(sql`smart_search.embedding <=> ${embedding}`)
|
||||
|
@ -178,6 +181,9 @@ export class SearchRepository implements ISearchRepository {
|
|||
.innerJoin('face_search', 'face_search.faceId', 'asset_faces.id')
|
||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null))
|
||||
.orderBy(sql`face_search.embedding <=> ${embedding}`)
|
||||
.limit(numResults),
|
||||
|
@ -228,6 +234,9 @@ export class SearchRepository implements ISearchRepository {
|
|||
.where('assets.isArchived', '=', false)
|
||||
.where('assets.type', '=', 'IMAGE')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.orderBy('city')
|
||||
.limit(1);
|
||||
|
||||
|
@ -245,6 +254,9 @@ export class SearchRepository implements ISearchRepository {
|
|||
.where('assets.isArchived', '=', false)
|
||||
.where('assets.type', '=', 'IMAGE')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.whereRef('exif.city', '>', 'cte.city')
|
||||
.orderBy('city')
|
||||
.limit(1)
|
||||
|
|
|
@ -25,6 +25,9 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
|||
.whereRef('shared_links.id', '=', 'shared_link__asset.sharedLinksId')
|
||||
.innerJoin('assets', 'assets.id', 'shared_link__asset.assetsId')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.selectAll('assets')
|
||||
.innerJoinLateral(
|
||||
(eb) => eb.selectFrom('exif').selectAll('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'),
|
||||
|
@ -50,6 +53,9 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
|||
.selectAll('assets')
|
||||
.whereRef('albums_assets_assets.assetsId', '=', 'assets.id')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.innerJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
|
@ -105,6 +111,9 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
|||
.selectFrom('assets')
|
||||
.whereRef('assets.id', '=', 'shared_link__asset.assetsId')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.selectAll('assets')
|
||||
.as('assets'),
|
||||
(join) => join.onTrue(),
|
||||
|
|
|
@ -30,6 +30,9 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
|||
)
|
||||
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||
).as('assets');
|
||||
};
|
||||
|
@ -62,7 +65,10 @@ export class StackRepository implements IStackRepository {
|
|||
.selectFrom('assets')
|
||||
.select('assets.id')
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id')
|
||||
.where('assets.deletedAt', 'is', null),
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null),
|
||||
).as('assets'),
|
||||
)
|
||||
.execute();
|
||||
|
|
|
@ -541,6 +541,11 @@ describe(AssetMediaService.name, () => {
|
|||
|
||||
it('should throw an error if the requested preview file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
|
||||
if (!assetStub.image.fileCreatedAt) {
|
||||
throw new Error('fileCreatedAt is missing');
|
||||
}
|
||||
|
||||
assetMock.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [
|
||||
|
@ -561,6 +566,11 @@ describe(AssetMediaService.name, () => {
|
|||
|
||||
it('should fall back to preview if the requested thumbnail file does not exist', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||
|
||||
if (!assetStub.image.fileCreatedAt) {
|
||||
throw new Error('fileCreatedAt is missing');
|
||||
}
|
||||
|
||||
assetMock.getById.mockResolvedValue({
|
||||
...assetStub.image,
|
||||
files: [
|
||||
|
|
|
@ -503,7 +503,7 @@ export class LibraryService extends BaseService {
|
|||
}
|
||||
|
||||
const mtime = stat.mtime;
|
||||
const isAssetModified = mtime.toISOString() !== asset.fileModifiedAt.toISOString();
|
||||
const isAssetModified = asset.fileModifiedAt === null || mtime.toISOString() !== asset.fileModifiedAt.toISOString();
|
||||
|
||||
if (asset.isOffline || isAssetModified) {
|
||||
this.logger.debug(`Asset was offline or modified, updating asset record ${asset.originalPath}`);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored';
|
||||
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
||||
import { Insertable } from 'kysely';
|
||||
|
@ -10,6 +10,7 @@ import { SystemConfig } from 'src/config';
|
|||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { Exif } from 'src/db';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AssetDatesDto } from 'src/dtos/asset.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
|
@ -162,6 +163,14 @@ export class MetadataService extends BaseService {
|
|||
|
||||
this.logger.verbose('Exif Tags', exifTags);
|
||||
|
||||
if (!asset.fileCreatedAt) {
|
||||
asset.fileCreatedAt = stats.birthtime;
|
||||
}
|
||||
|
||||
if (!asset.fileModifiedAt) {
|
||||
asset.fileModifiedAt = stats.mtime;
|
||||
}
|
||||
|
||||
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
||||
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
||||
|
||||
|
@ -451,6 +460,7 @@ export class MetadataService extends BaseService {
|
|||
}
|
||||
} else {
|
||||
const motionAssetId = this.cryptoRepository.randomUUID();
|
||||
|
||||
const dates = this.getDates(asset, tags);
|
||||
motionAsset = await this.assetRepository.create({
|
||||
id: motionAssetId,
|
||||
|
@ -569,7 +579,17 @@ export class MetadataService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
private getDates(asset: AssetEntity, exifTags: ImmichTags) {
|
||||
private getDates(asset: AssetEntity, exifTags: ImmichTags): AssetDatesDto {
|
||||
// We first assert that fileCreatedAt and fileModifiedAt are not null since that should be set to a non-null value before calling this function
|
||||
if (asset.fileCreatedAt === null) {
|
||||
this.logger.warn(`Asset ${asset.id} has no file creation date`);
|
||||
throw new BadRequestException(`Asset ${asset.id} has no file creation date`);
|
||||
}
|
||||
if (asset.fileModifiedAt === null) {
|
||||
this.logger.warn(`Asset ${asset.id} has no file modification date`);
|
||||
throw new BadRequestException(`Asset ${asset.id} has no file modification date`);
|
||||
}
|
||||
|
||||
const dateTime = firstDateTime(exifTags as Maybe<Tags>, EXIF_DATE_TAGS);
|
||||
this.logger.verbose(`Asset ${asset.id} date time is ${dateTime}`);
|
||||
|
||||
|
@ -598,7 +618,11 @@ export class MetadataService extends BaseService {
|
|||
localDateTime = earliestDate;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`);
|
||||
if (localDateTime) {
|
||||
this.logger.verbose(`Asset ${asset.id} has a local time of ${localDateTime.toISOString()}`);
|
||||
} else {
|
||||
this.logger.verbose(`Asset ${asset.id} has no time set`);
|
||||
}
|
||||
|
||||
let modifyDate = asset.fileModifiedAt;
|
||||
try {
|
||||
|
|
|
@ -179,6 +179,10 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
|
||||
|
||||
if (asset.fileCreatedAt == null) {
|
||||
throw new Error('fileCreatedAt must be set');
|
||||
}
|
||||
|
||||
expect(moveMock.create).toHaveBeenCalledWith({
|
||||
entityId: asset.id,
|
||||
newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`,
|
||||
|
@ -199,6 +203,10 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS);
|
||||
|
||||
if (asset.fileCreatedAt == null) {
|
||||
throw new Error('fileCreatedAt must be set');
|
||||
}
|
||||
|
||||
const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0');
|
||||
expect(moveMock.create).toHaveBeenCalledWith({
|
||||
entityId: asset.id,
|
||||
|
|
|
@ -310,6 +310,12 @@ export class StorageTemplateService extends BaseService {
|
|||
|
||||
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const zone = asset.exifInfo?.timeZone || systemTimeZone;
|
||||
|
||||
if (!asset.fileCreatedAt) {
|
||||
this.logger.log(`Asset ${asset.id} is missing fileCreatedAt, skipping storage template migration`);
|
||||
throw new Error(`Missing fileCreatedAt for asset ${asset.id}`);
|
||||
}
|
||||
|
||||
const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone });
|
||||
|
||||
for (const token of Object.values(storageTokens).flat()) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export const getAssetDateTime = (asset: AssetEntity | undefined) => {
|
||||
return asset?.exifInfo?.dateTimeOriginal || asset?.fileCreatedAt;
|
||||
export const getAssetDateTime = (asset: AssetEntity | undefined): Date | undefined => {
|
||||
return (asset?.exifInfo?.dateTimeOriginal || asset?.fileCreatedAt) ?? undefined;
|
||||
};
|
||||
|
|
|
@ -29,6 +29,8 @@ type TimeZoneTest = {
|
|||
description: string;
|
||||
serverTimeZone?: string;
|
||||
exifData: Record<string, any>;
|
||||
fileCreatedAt: Date;
|
||||
fileModifiedAt: Date;
|
||||
expected: {
|
||||
localDateTime: string;
|
||||
dateTimeOriginal: string;
|
||||
|
@ -58,6 +60,8 @@ describe(MetadataService.name, () => {
|
|||
const timeZoneTests: TimeZoneTest[] = [
|
||||
{
|
||||
description: 'should handle no time zone information',
|
||||
fileCreatedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
fileModifiedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
exifData: {
|
||||
DateTimeOriginal: '2022:01:01 00:00:00',
|
||||
},
|
||||
|
@ -69,6 +73,8 @@ describe(MetadataService.name, () => {
|
|||
},
|
||||
{
|
||||
description: 'should handle no time zone information and server behind UTC',
|
||||
fileCreatedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
fileModifiedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
serverTimeZone: 'America/Los_Angeles',
|
||||
exifData: {
|
||||
DateTimeOriginal: '2022:01:01 00:00:00',
|
||||
|
@ -81,6 +87,8 @@ describe(MetadataService.name, () => {
|
|||
},
|
||||
{
|
||||
description: 'should handle no time zone information and server ahead of UTC',
|
||||
fileCreatedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
fileModifiedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
serverTimeZone: 'Europe/Brussels',
|
||||
exifData: {
|
||||
DateTimeOriginal: '2022:01:01 00:00:00',
|
||||
|
@ -93,6 +101,8 @@ describe(MetadataService.name, () => {
|
|||
},
|
||||
{
|
||||
description: 'should handle no time zone information and server ahead of UTC in the summer',
|
||||
fileCreatedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
fileModifiedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
serverTimeZone: 'Europe/Brussels',
|
||||
exifData: {
|
||||
DateTimeOriginal: '2022:06:01 00:00:00',
|
||||
|
@ -105,6 +115,8 @@ describe(MetadataService.name, () => {
|
|||
},
|
||||
{
|
||||
description: 'should handle a +13:00 time zone',
|
||||
fileCreatedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
fileModifiedAt: new Date('2022-01-01T00:00:00.000Z'),
|
||||
exifData: {
|
||||
DateTimeOriginal: '2022:01:01 00:00:00+13:00',
|
||||
},
|
||||
|
@ -116,26 +128,32 @@ describe(MetadataService.name, () => {
|
|||
},
|
||||
];
|
||||
|
||||
it.each(timeZoneTests)('$description', async ({ exifData, serverTimeZone, expected }) => {
|
||||
process.env.TZ = serverTimeZone ?? undefined;
|
||||
it.each(timeZoneTests)(
|
||||
'$description',
|
||||
async ({ exifData, serverTimeZone, expected, fileCreatedAt, fileModifiedAt }) => {
|
||||
// TODO: the TZ environment variable is no longer used, remove it
|
||||
process.env.TZ = serverTimeZone ?? undefined;
|
||||
|
||||
const { filePath } = await createTestFile(exifData);
|
||||
assetMock.getByIds.mockResolvedValue([{ id: 'asset-1', originalPath: filePath } as AssetEntity]);
|
||||
const { filePath } = await createTestFile(exifData);
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
{ id: 'asset-1', originalPath: filePath, fileCreatedAt, fileModifiedAt } as AssetEntity,
|
||||
]);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: 'asset-1' });
|
||||
await sut.handleMetadataExtraction({ id: 'asset-1' });
|
||||
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateTimeOriginal: new Date(expected.dateTimeOriginal),
|
||||
timeZone: expected.timeZone,
|
||||
}),
|
||||
);
|
||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateTimeOriginal: new Date(expected.dateTimeOriginal),
|
||||
timeZone: expected.timeZone,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(assetMock.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
localDateTime: new Date(expected.localDateTime),
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(assetMock.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
localDateTime: new Date(expected.localDateTime),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue