2024-03-20 19:32:04 +01:00
import { Injectable } from '@nestjs/common' ;
import { InjectRepository } from '@nestjs/typeorm' ;
import { DateTime } from 'luxon' ;
import path from 'node:path' ;
2024-03-20 15:04:03 -05:00
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' ;
2024-03-20 21:42:58 +01:00
import { OptionalBetween , paginate , paginatedBuilder , searchAssetBuilder } from 'src/infra/infra.utils' ;
import { Instrumentation } from 'src/infra/instrumentation' ;
2023-05-21 08:26:06 +02:00
import {
2023-12-08 11:15:46 -05:00
AssetBuilderOptions ,
2023-10-04 18:11:11 -04:00
AssetCreate ,
2023-12-08 11:15:46 -05:00
AssetExploreFieldOptions ,
2024-03-10 22:30:57 -04:00
AssetPathEntity ,
2023-07-14 09:30:17 -04:00
AssetStats ,
AssetStatsOptions ,
2024-03-19 22:42:10 -04:00
AssetUpdateAllOptions ,
AssetUpdateOptions ,
2023-05-21 08:26:06 +02:00
IAssetRepository ,
LivePhotoSearchOptions ,
MapMarker ,
MapMarkerSearchOptions ,
2023-12-08 11:15:46 -05:00
MetadataSearchOptions ,
2023-10-04 18:11:11 -04:00
MonthDay ,
2023-08-04 17:07:15 -04:00
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 ,
2024-03-20 21:42:58 +01:00
} from 'src/interfaces/asset.repository' ;
import { AssetSearchOptions , SearchExploreItem } from 'src/interfaces/search.repository' ;
2024-03-20 15:04:03 -05:00
import { Paginated , PaginationMode , PaginationOptions } from 'src/utils' ;
2024-01-18 00:08:48 -05:00
import {
Brackets ,
FindOptionsRelations ,
FindOptionsSelect ,
FindOptionsWhere ,
In ,
IsNull ,
Not ,
Repository ,
} from 'typeorm' ;
2023-11-14 17:47:15 -05:00
2023-08-04 17:07:15 -04:00
const truncateMap : Record < TimeBucketSize , string > = {
[ TimeBucketSize . DAY ] : 'day' ,
[ TimeBucketSize . MONTH ] : 'month' ,
} ;
2023-10-16 14:11:50 -04:00
const dateTrunc = ( options : TimeBucketOptions ) = >
2023-10-27 15:34:01 -05:00
` (date_trunc(' ${
truncateMap [ options . size ]
} ', (asset."localDateTime" at time zone ' UTC ')) at time zone ' UTC ' ) : : timestamptz ` ;
2023-10-06 08:12:09 -04:00
2024-03-12 01:19:12 -04:00
@Instrumentation ( )
2023-02-25 09:12:03 -05:00
@Injectable ( )
export class AssetRepository implements IAssetRepository {
2023-09-04 22:25:31 -04:00
constructor (
@InjectRepository ( AssetEntity ) private repository : Repository < AssetEntity > ,
@InjectRepository ( ExifEntity ) private exifRepository : Repository < ExifEntity > ,
2023-11-09 17:55:00 -08:00
@InjectRepository ( AssetJobStatusEntity ) private jobStatusRepository : Repository < AssetJobStatusEntity > ,
2023-12-08 11:15:46 -05:00
@InjectRepository ( SmartInfoEntity ) private smartInfoRepository : Repository < SmartInfoEntity > ,
2023-09-04 22:25:31 -04:00
) { }
async upsertExif ( exif : Partial < ExifEntity > ) : Promise < void > {
await this . exifRepository . upsert ( exif , { conflictPaths : [ 'assetId' ] } ) ;
}
2023-02-25 09:12:03 -05:00
2023-11-09 17:55:00 -08:00
async upsertJobStatus ( jobStatus : Partial < AssetJobStatusEntity > ) : Promise < void > {
await this . jobStatusRepository . upsert ( jobStatus , { conflictPaths : [ 'assetId' ] } ) ;
}
2023-10-04 18:11:11 -04:00
create ( asset : AssetCreate ) : Promise < AssetEntity > {
2023-09-20 13:16:33 +02:00
return this . repository . save ( asset ) ;
}
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ DummyValue . UUID , DummyValue . DATE ] } )
2023-06-14 20:47:18 -05:00
getByDate ( ownerId : string , date : Date ) : Promise < AssetEntity [ ] > {
2023-06-15 14:05:30 -04:00
// For reference of a correct approach although slower
2023-06-14 20:47:18 -05:00
// 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 ,
2023-06-19 10:12:18 -04:00
resizePath : Not ( IsNull ( ) ) ,
2023-06-15 14:05:30 -04:00
fileCreatedAt : OptionalBetween ( date , DateTime . fromJSDate ( date ) . plus ( { day : 1 } ) . toJSDate ( ) ) ,
2023-06-14 20:47:18 -05:00
} ,
relations : {
exifInfo : true ,
} ,
order : {
fileCreatedAt : 'DESC' ,
} ,
} ) ;
}
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ DummyValue . UUID , { day : 1 , month : 1 } ] } )
2024-03-18 14:46:52 -05:00
getByDayOfYear ( ownerIds : string [ ] , { day , month } : MonthDay ) : Promise < AssetEntity [ ] > {
2023-10-04 18:11:11 -04:00
return this . repository
. createQueryBuilder ( 'entity' )
. where (
2024-03-18 14:46:52 -05:00
` entity.ownerId IN (:...ownerIds)
2023-10-04 18:11:11 -04:00
AND entity . isVisible = true
AND entity . isArchived = false
AND entity . resizePath IS NOT NULL
2023-10-06 08:12:09 -04:00
AND EXTRACT ( DAY FROM entity . localDateTime AT TIME ZONE 'UTC' ) = : day
AND EXTRACT ( MONTH FROM entity . localDateTime AT TIME ZONE 'UTC' ) = : month ` ,
2023-10-04 18:11:11 -04:00
{
2024-03-18 14:46:52 -05:00
ownerIds ,
2023-10-04 18:11:11 -04:00
day ,
month ,
} ,
)
2023-10-19 14:33:35 -05:00
. leftJoinAndSelect ( 'entity.exifInfo' , 'exifInfo' )
2023-10-04 18:11:11 -04:00
. orderBy ( 'entity.localDateTime' , 'DESC' )
. getMany ( ) ;
}
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ [ DummyValue . UUID ] ] } )
2024-01-06 20:36:12 -05:00
@ChunkedArray ( )
2024-01-18 00:08:48 -05:00
getByIds (
ids : string [ ] ,
relations? : FindOptionsRelations < AssetEntity > ,
select? : FindOptionsSelect < AssetEntity > ,
) : Promise < AssetEntity [ ] > {
2024-03-14 01:58:09 -04:00
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 : {
2023-03-18 08:44:42 -05:00
exifInfo : true ,
smartInfo : true ,
tags : true ,
2023-05-17 13:07:17 -04:00
faces : {
person : true ,
} ,
2024-01-27 18:52:14 +00:00
stack : {
assets : true ,
} ,
2024-03-14 01:58:09 -04:00
} ,
2023-10-06 07:01:14 +00:00
withDeleted : true ,
2023-03-18 08:44:42 -05:00
} ) ;
}
2023-09-20 13:16:33 +02:00
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ DummyValue . UUID ] } )
2023-02-25 09:12:03 -05:00
async deleteAll ( ownerId : string ) : Promise < void > {
await this . repository . delete ( { ownerId } ) ;
}
2023-06-30 12:24:28 -04:00
getByAlbumId ( pagination : PaginationOptions , albumId : string ) : Paginated < AssetEntity > {
return paginate ( this . repository , pagination , {
where : {
albums : {
id : albumId ,
} ,
} ,
relations : {
albums : true ,
exifInfo : true ,
} ,
} ) ;
}
2024-02-18 13:22:25 -05:00
getByUserId (
pagination : PaginationOptions ,
userId : string ,
options : Omit < AssetSearchOptions , 'userIds' > = { } ,
) : Paginated < AssetEntity > {
return this . getAll ( pagination , { . . . options , userIds : [ userId ] } ) ;
2023-06-30 12:24:28 -04:00
}
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ [ DummyValue . UUID ] ] } )
2024-03-10 22:30:57 -04:00
getLibraryAssetPaths ( pagination : PaginationOptions , libraryId : string ) : Paginated < AssetPathEntity > {
return paginate ( this . repository , pagination , {
select : { id : true , originalPath : true , isOffline : true } ,
where : { library : { id : libraryId } } ,
2023-09-20 13:16:33 +02:00
} ) ;
}
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ DummyValue . UUID , DummyValue . STRING ] } )
2023-09-20 13:16:33 +02:00
getByLibraryIdAndOriginalPath ( libraryId : string , originalPath : string ) : Promise < AssetEntity | null > {
return this . repository . findOne ( {
where : { library : { id : libraryId } , originalPath : originalPath } ,
} ) ;
}
2024-03-06 22:23:10 -05:00
@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 } ,
) ;
}
2023-05-22 20:05:06 +02:00
getAll ( pagination : PaginationOptions , options : AssetSearchOptions = { } ) : Paginated < AssetEntity > {
2024-02-12 20:50:47 -05:00
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 ,
2023-03-02 21:47:08 -05:00
} ) ;
2023-02-25 09:12:03 -05:00
}
2023-11-25 15:46:20 +00:00
/ * *
* Get assets by device ' s Id on the database
* @param ownerId
* @param deviceId
*
* @returns Promise < string [ ] > - Array of assetIds belong to the device
* /
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ DummyValue . UUID , DummyValue . STRING ] } )
2023-11-25 15:46:20 +00:00
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 ) ;
}
2023-11-30 10:10:30 -05:00
@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 ,
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 ,
} ) ;
}
2023-11-30 10:10:30 -05:00
@GenerateSql ( { params : [ [ DummyValue . UUID ] , { deviceId : DummyValue.STRING } ] } )
2024-01-06 20:36:12 -05:00
@Chunked ( )
2024-03-19 22:42:10 -04:00
async updateAll ( ids : string [ ] , options : AssetUpdateAllOptions ) : Promise < void > {
2023-08-16 16:04:55 -04:00
await this . repository . update ( { id : In ( ids ) } , options ) ;
}
2024-01-06 20:36:12 -05:00
@Chunked ( )
2023-10-06 07:01:14 +00:00
async softDeleteAll ( ids : string [ ] ) : Promise < void > {
await this . repository . softDelete ( { id : In ( ids ) , isExternal : false } ) ;
}
2024-01-06 20:36:12 -05:00
@Chunked ( )
2023-10-06 07:01:14 +00:00
async restoreAll ( ids : string [ ] ) : Promise < void > {
await this . repository . restore ( { id : In ( ids ) } ) ;
}
2024-03-19 22:42:10 -04:00
async update ( asset : AssetUpdateOptions ) : Promise < void > {
await this . repository . update ( asset . id , asset ) ;
2023-02-25 09:12:03 -05:00
}
2023-10-06 07:01:14 +00:00
async remove ( asset : AssetEntity ) : Promise < void > {
await this . repository . remove ( asset ) ;
}
2024-02-12 20:50:47 -05:00
@GenerateSql ( { params : [ DummyValue . UUID , DummyValue . BUFFER ] } )
2023-09-12 23:46:37 -04:00
getByChecksum ( userId : string , checksum : Buffer ) : Promise < AssetEntity | null > {
return this . repository . findOne ( { where : { ownerId : userId , checksum } } ) ;
}
2023-04-04 00:48:05 -04:00
findLivePhotoMatch ( options : LivePhotoSearchOptions ) : Promise < AssetEntity | null > {
const { ownerId , otherAssetId , livePhotoCID , type } = options ;
2023-02-25 09:12:03 -05:00
return this . repository . findOne ( {
where : {
id : Not ( otherAssetId ) ,
2023-04-04 00:48:05 -04:00
ownerId ,
2023-02-25 09:12:03 -05:00
type ,
exifInfo : {
livePhotoCID ,
} ,
} ,
relations : {
exifInfo : true ,
} ,
} ) ;
}
2023-03-20 11:55:28 -04:00
2023-11-30 10:10:30 -05:00
@GenerateSql (
. . . Object . values ( WithProperty )
. filter ( ( property ) = > property !== WithProperty . IS_OFFLINE )
. map ( ( property ) = > ( {
name : property ,
params : [ DummyValue . PAGINATION , property ] ,
} ) ) ,
)
2023-05-22 20:05:06 +02:00
getWithout ( pagination : PaginationOptions , property : WithoutProperty ) : Paginated < AssetEntity > {
2023-03-20 11:55:28 -04:00
let relations : FindOptionsRelations < AssetEntity > = { } ;
let where : FindOptionsWhere < AssetEntity > | FindOptionsWhere < AssetEntity > [ ] = { } ;
switch ( property ) {
2024-02-02 04:18:00 +01:00
case WithoutProperty . THUMBNAIL : {
2023-03-20 11:55:28 -04:00
where = [
{ resizePath : IsNull ( ) , isVisible : true } ,
{ resizePath : '' , isVisible : true } ,
{ webpPath : IsNull ( ) , isVisible : true } ,
{ webpPath : '' , isVisible : true } ,
2023-06-17 23:22:31 -04:00
{ thumbhash : IsNull ( ) , isVisible : true } ,
2023-03-20 11:55:28 -04:00
] ;
break ;
2024-02-02 04:18:00 +01:00
}
2023-03-20 11:55:28 -04:00
2024-02-02 04:18:00 +01:00
case WithoutProperty . ENCODED_VIDEO : {
2023-03-20 11:55:28 -04:00
where = [
{ type : AssetType . VIDEO , encodedVideoPath : IsNull ( ) } ,
{ type : AssetType . VIDEO , encodedVideoPath : '' } ,
] ;
break ;
2024-02-02 04:18:00 +01:00
}
2023-03-20 11:55:28 -04:00
2024-02-02 04:18:00 +01:00
case WithoutProperty . EXIF : {
2023-03-20 11:55:28 -04:00
relations = {
exifInfo : true ,
2024-01-12 19:39:45 -05:00
jobStatus : true ,
2023-03-20 11:55:28 -04:00
} ;
where = {
isVisible : true ,
2024-01-12 19:39:45 -05:00
jobStatus : {
metadataExtractedAt : IsNull ( ) ,
2023-03-20 11:55:28 -04:00
} ,
} ;
break ;
2024-02-02 04:18:00 +01:00
}
2023-03-20 11:55:28 -04:00
2024-02-02 04:18:00 +01:00
case WithoutProperty . SMART_SEARCH : {
2023-03-20 11:55:28 -04:00
relations = {
2023-12-08 11:15:46 -05:00
smartSearch : true ,
2023-03-20 11:55:28 -04:00
} ;
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 ( ) ,
2023-03-20 11:55:28 -04:00
} ,
} ;
break ;
2024-02-02 04:18:00 +01:00
}
2023-03-20 11:55:28 -04:00
2024-02-02 04:18:00 +01:00
case WithoutProperty . OBJECT_TAGS : {
2023-03-20 11:55:28 -04:00
relations = {
smartInfo : true ,
} ;
where = {
2023-05-31 11:00:37 -04:00
resizePath : Not ( IsNull ( ) ) ,
2023-03-20 11:55:28 -04:00
isVisible : true ,
smartInfo : {
tags : IsNull ( ) ,
} ,
} ;
break ;
2024-02-02 04:18:00 +01:00
}
2023-03-20 11:55:28 -04:00
2024-02-02 04:18:00 +01:00
case WithoutProperty . FACES : {
2023-05-17 13:07:17 -04:00
relations = {
faces : true ,
2023-11-09 17:55:00 -08:00
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 ( ) ,
} ,
2023-11-09 17:55:00 -08:00
jobStatus : {
facesRecognizedAt : IsNull ( ) ,
} ,
2023-05-17 13:07:17 -04:00
} ;
break ;
2024-02-02 04:18:00 +01:00
}
2023-05-17 13:07:17 -04:00
2024-02-02 04:18:00 +01:00
case WithoutProperty . PERSON : {
2024-01-18 00:08:48 -05:00
relations = {
faces : true ,
} ;
where = {
resizePath : Not ( IsNull ( ) ) ,
isVisible : true ,
faces : {
assetId : Not ( IsNull ( ) ) ,
personId : IsNull ( ) ,
} ,
} ;
break ;
2024-02-02 04:18:00 +01:00
}
2024-01-18 00:08:48 -05:00
2024-02-02 04:18:00 +01: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 ;
2024-02-02 04:18:00 +01:00
}
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
2024-02-02 04:18:00 +01:00
default : {
2023-03-20 11:55:28 -04:00
throw new Error ( ` Invalid getWithout property: ${ property } ` ) ;
2024-02-02 04:18:00 +01:00
}
2023-03-20 11:55:28 -04:00
}
2023-05-22 20:05:06 +02:00
return paginate ( this . repository , pagination , {
2023-03-20 11:55:28 -04:00
relations ,
where ,
2023-05-22 20:05:06 +02:00
order : {
// Ensures correct order when paginating
createdAt : 'ASC' ,
} ,
2023-03-20 11:55:28 -04:00
} ) ;
}
2023-03-26 04:46:48 +02:00
2023-09-20 13:16:33 +02:00
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 ) {
2024-02-02 04:18:00 +01:00
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 ;
2024-02-02 04:18:00 +01:00
}
case WithProperty . IS_OFFLINE : {
2023-09-20 13:16:33 +02:00
if ( ! libraryId ) {
throw new Error ( 'Library id is required when finding offline assets' ) ;
}
where = [ { isOffline : true , libraryId : libraryId } ] ;
break ;
2024-02-02 04:18:00 +01:00
}
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
2024-02-02 04:18:00 +01: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 } ` ) ;
2024-02-02 04:18:00 +01:00
}
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' ,
} ,
} ) ;
}
2023-03-26 04:46:48 +02:00
getFirstAssetForAlbumId ( albumId : string ) : Promise < AssetEntity | null > {
return this . repository . findOne ( {
where : { albums : { id : albumId } } ,
order : { fileCreatedAt : 'DESC' } ,
} ) ;
}
2023-05-21 08:26:06 +02:00
2023-06-21 04:00:59 +03:00
getLastUpdatedAssetForAlbumId ( albumId : string ) : Promise < AssetEntity | null > {
return this . repository . findOne ( {
where : { albums : { id : albumId } } ,
order : { updatedAt : 'DESC' } ,
} ) ;
}
2024-02-14 16:07:00 +01:00
async getMapMarkers ( ownerIds : string [ ] , options : MapMarkerSearchOptions = { } ) : Promise < MapMarker [ ] > {
2023-10-04 15:51:07 +02:00
const { isArchived , isFavorite , fileCreatedAfter , fileCreatedBefore } = options ;
2023-05-21 08:26:06 +02:00
const assets = await this . repository . find ( {
select : {
id : true ,
exifInfo : {
2024-03-03 16:42:17 -05:00
city : true ,
state : true ,
country : true ,
2023-05-21 08:26:06 +02:00
latitude : true ,
longitude : true ,
} ,
} ,
where : {
2024-02-14 16:07:00 +01:00
ownerId : In ( [ . . . ownerIds ] ) ,
2023-05-21 08:26:06 +02:00
isVisible : true ,
2023-10-04 15:51:07 +02:00
isArchived ,
2023-05-21 08:26:06 +02:00
exifInfo : {
2023-06-02 21:04:07 +02:00
latitude : Not ( IsNull ( ) ) ,
longitude : Not ( IsNull ( ) ) ,
2023-05-21 08:26:06 +02:00
} ,
isFavorite ,
2023-05-25 18:47:52 +02:00
fileCreatedAt : OptionalBetween ( fileCreatedAfter , fileCreatedBefore ) ,
2023-05-21 08:26:06 +02:00
} ,
relations : {
exifInfo : true ,
} ,
order : {
fileCreatedAt : 'DESC' ,
} ,
} ) ;
return assets . map ( ( asset ) = > ( {
id : asset.id ,
lat : asset.exifInfo ! . latitude ! ,
lon : asset.exifInfo ! . longitude ! ,
2024-03-03 16:42:17 -05:00
city : asset.exifInfo ! . city ,
state : asset.exifInfo ! . state ,
country : asset.exifInfo ! . country ,
2023-05-21 08:26:06 +02:00
} ) ) ;
}
2023-07-14 09:30:17 -04:00
async getStatistics ( ownerId : string , options : AssetStatsOptions ) : Promise < AssetStats > {
2024-03-05 23:23:06 +01:00
let builder = this . repository
2023-07-14 09:30:17 -04:00
. createQueryBuilder ( 'asset' )
. select ( ` COUNT(asset.id) ` , 'count' )
. addSelect ( ` asset.type ` , 'type' )
. where ( '"ownerId" = :ownerId' , { ownerId } )
. andWhere ( 'asset.isVisible = true' )
. groupBy ( 'asset.type' ) ;
2023-10-06 07:01:14 +00:00
const { isArchived , isFavorite , isTrashed } = options ;
2023-07-14 09:30:17 -04:00
if ( isArchived !== undefined ) {
builder = builder . andWhere ( ` asset.isArchived = :isArchived ` , { isArchived } ) ;
}
if ( isFavorite !== undefined ) {
builder = builder . andWhere ( ` asset.isFavorite = :isFavorite ` , { isFavorite } ) ;
}
2023-10-06 07:01:14 +00:00
if ( isTrashed !== undefined ) {
builder = builder . withDeleted ( ) . andWhere ( ` asset.deletedAt is not null ` ) ;
}
2023-07-14 09:30:17 -04:00
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 ;
}
2023-08-04 17:07:15 -04:00
2023-09-23 17:28:55 +02:00
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
2023-10-04 19:51:44 +02:00
OFFSET FLOOR ( RANDOM ( ) * ( SELECT GREATEST ( COUNT ( * ) - $2 , 0 ) FROM ASSETS WHERE "ownerId" = $1 ) ) LIMIT $2 ` ,
2023-09-23 17:28:55 +02:00
[ ownerId , count ] ,
) ;
}
2024-01-03 23:32:52 -05:00
@GenerateSql ( { params : [ { size : TimeBucketSize.MONTH } ] } )
2023-08-11 12:00:51 -04:00
getTimeBuckets ( options : TimeBucketOptions ) : Promise < TimeBucketItem [ ] > {
2023-10-16 14:11:50 -04:00
const truncated = dateTrunc ( options ) ;
2023-08-11 12:00:51 -04:00
return this . getBuilder ( options )
2023-08-04 17:07:15 -04:00
. select ( ` COUNT(asset.id)::int ` , 'count' )
2023-10-16 14:11:50 -04:00
. addSelect ( truncated , 'timeBucket' )
. groupBy ( truncated )
2024-03-14 17:45:03 +01:00
. orderBy ( truncated , options . order === AssetOrder . ASC ? 'ASC' : 'DESC' )
2023-08-04 17:07:15 -04:00
. getRawMany ( ) ;
}
2024-01-03 23:32:52 -05:00
@GenerateSql ( { params : [ DummyValue . TIME_BUCKET , { size : TimeBucketSize.MONTH } ] } )
2023-11-03 21:33:15 -04:00
getTimeBucket ( timeBucket : string , options : TimeBucketOptions ) : Promise < AssetEntity [ ] > {
2023-10-16 14:11:50 -04:00
const truncated = dateTrunc ( options ) ;
2023-10-06 08:12:09 -04:00
return (
this . getBuilder ( options )
2024-01-17 16:08:38 -05:00
. andWhere ( ` ${ truncated } = :timeBucket ` , { timeBucket : timeBucket.replace ( /^[+-]/ , '' ) } )
2023-10-06 08:12:09 -04:00
// First sort by the day in localtime (put it in the right bucket)
2023-10-16 14:11:50 -04:00
. orderBy ( truncated , 'DESC' )
2023-10-06 08:12:09 -04:00
// and then sort by the actual time
2024-03-14 17:45:03 +01:00
. addOrderBy ( 'asset.fileCreatedAt' , options . order === AssetOrder . ASC ? 'ASC' : 'DESC' )
2023-10-06 08:12:09 -04:00
. getMany ( )
) ;
2023-08-04 17:07:15 -04:00
}
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' )
2023-12-17 12:04:35 -05:00
. 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' )
2023-12-17 12:04:35 -05:00
. 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 ;
2023-08-04 17:07:15 -04:00
2024-01-17 16:08:38 -05:00
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 } ) ;
}
2024-01-27 18:52:14 +00:00
let stackJoined = false ;
2023-12-08 11:15:46 -05:00
if ( exifInfo !== false ) {
2024-01-27 18:52:14 +00:00
stackJoined = true ;
builder = builder
. leftJoinAndSelect ( 'asset.exifInfo' , 'exifInfo' )
. leftJoinAndSelect ( 'asset.stack' , 'stack' )
. leftJoinAndSelect ( 'stack.assets' , 'stackedAssets' ) ;
2023-12-08 11:15:46 -05:00
}
2023-08-04 17:07:15 -04:00
if ( albumId ) {
builder = builder . leftJoin ( 'asset.albums' , 'album' ) . andWhere ( 'album.id = :albumId' , { albumId } ) ;
}
2023-11-11 15:06:19 -06:00
if ( userIds ) {
builder = builder . andWhere ( 'asset.ownerId IN (:...userIds )' , { userIds } ) ;
2023-08-11 12:00:51 -04:00
}
2023-11-11 15:06:19 -06:00
if ( isArchived !== undefined ) {
2023-08-04 17:07:15 -04:00
builder = builder . andWhere ( 'asset.isArchived = :isArchived' , { isArchived } ) ;
}
if ( isFavorite !== undefined ) {
builder = builder . andWhere ( 'asset.isFavorite = :isFavorite' , { isFavorite } ) ;
}
2023-10-06 07:01:14 +00:00
if ( isTrashed !== undefined ) {
builder = builder . andWhere ( ` asset.deletedAt ${ isTrashed ? 'IS NOT NULL' : 'IS NULL' } ` ) . withDeleted ( ) ;
}
2023-08-05 09:58:52 -04:00
if ( personId !== undefined ) {
builder = builder
. innerJoin ( 'asset.faces' , 'faces' )
. innerJoin ( 'faces.person' , 'person' )
. andWhere ( 'person.id = :personId' , { personId } ) ;
}
2023-10-27 15:34:01 -05:00
if ( withStacked ) {
2024-01-27 18:52:14 +00:00
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-27 15:34:01 -05:00
}
2023-10-22 02:38:07 +00:00
2023-08-04 17:07:15 -04:00
return builder ;
}
2023-12-08 11:15:46 -05:00
2024-01-01 23:25:22 +01:00
@GenerateSql ( { params : [ DummyValue . STRING , [ DummyValue . UUID ] , { numResults : 250 } ] } )
async searchMetadata (
query : string ,
userIds : string [ ] ,
{ numResults } : MetadataSearchOptions ,
) : Promise < AssetEntity [ ] > {
2023-12-17 21:16:08 -05:00
const rows = await this . getBuilder ( {
2024-01-01 23:25:22 +01:00
userIds : userIds ,
2023-12-17 21:16:08 -05:00
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' )
2023-12-17 21:16:08 -05:00
. 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
)
2024-01-04 21:47:09 +01:00
. addOrderBy ( 'asset.fileCreatedAt' , 'DESC' )
2023-12-08 11:15:46 -05:00
. limit ( numResults )
. getRawMany ( ) ;
return rows . map (
2023-12-18 07:13:36 -06:00
( {
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 ,
2023-12-18 07:13:36 -06:00
country ,
dateTimeOriginal ,
2023-12-08 11:15:46 -05:00
description ,
2023-12-18 07:13:36 -06:00
exifImageHeight ,
exifImageWidth ,
exposureTime ,
fNumber ,
fileSizeInByte ,
focalLength ,
iso ,
latitude ,
lensModel ,
longitude ,
2023-12-08 11:15:46 -05:00
make ,
2023-12-18 07:13:36 -06:00
model ,
modifyDate ,
projectionType ,
state ,
timeZone ,
2023-12-08 11:15:46 -05:00
} ,
smartInfo : {
tags ,
objects ,
} ,
. . . assetInfo ,
} ) as AssetEntity ,
) ;
}
2023-02-25 09:12:03 -05:00
}