1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-11-21 17:56:45 +02:00

Updates for new ignore timezone setting

This commit is contained in:
grasdk 2024-05-11 15:26:38 +02:00
parent 517f483ffa
commit bc54dc2a3a
23 changed files with 133 additions and 69 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -364,7 +364,11 @@ export class SearchManager {
for (const sort of sortings) {
switch (sort.method) {
case SortByTypes.Date:
query.addOrderBy('media.metadata.creationDate', sort.ascending ? 'ASC' : 'DESC'); //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). If taken into account, it will alter the sort order. Probably should not be done.
if (Config.Gallery.ignoreTimestampOffset === true) {
query.addOrderBy('media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)', sort.ascending ? 'ASC' : 'DESC');
} else {
query.addOrderBy('media.metadata.creationDate', sort.ascending ? 'ASC' : 'DESC');
}
break;
case SortByTypes.Rating:
query.addOrderBy('media.metadata.rating', sort.ascending ? 'ASC' : 'DESC');
@ -562,15 +566,17 @@ export class SearchManager {
const textParam: { [key: string]: unknown } = {};
textParam['from' + queryId] = (query as FromDateSearch).value;
q.where(
`media.metadata.creationDate ${relation} :from${queryId}`, //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-).
//Example: -600 means in the database UTC-10:00. The time 20:00 in the evening in the UTC-10 timezone, is actually 06:00 the next morning
//in UTC+00:00. To make search take that into account, one can subtract the offset from the creationDate to "pretend" the photo is taken
//in UTC time. Subtracting -600 minutes (because it's the -10:00 timezone), corresponds to adding 10 hours to the photo's timestamp, thus
//bringing it into the next day as if it was taken at UTC+00:00. Similarly subtracting a positive timezone from a timestamp will "pretend"
//the photo is taken earlier in time (e.g. subtracting 300 from the UTC+05:00 timezone).
textParam
);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) ${relation} :from${queryId}`,
textParam
);
} else {
q.where(
`media.metadata.creationDate ${relation} :from${queryId}`,
textParam
);
}
return q;
});
@ -589,10 +595,18 @@ export class SearchManager {
const textParam: { [key: string]: unknown } = {};
textParam['to' + queryId] = (query as ToDateSearch).value;
q.where(
`media.metadata.creationDate ${relation} :to${queryId}`, //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
textParam
);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) ${relation} :to${queryId}`,
textParam
);
} else {
q.where(
`media.metadata.creationDate ${relation} :to${queryId}`,
textParam
);
}
return q;
});
@ -793,18 +807,25 @@ export class SearchManager {
textParam['to' + queryId] = to.getTime();
textParam['from' + queryId] = from.getTime();
if (tq.negate) {
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) >= :to${queryId}`,
textParam
).orWhere(`media.metadata.creationDate < :from${queryId}`,
textParam);
} else {
q.where(
`media.metadata.creationDate >= :to${queryId}`, //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
textParam
).orWhere(`media.metadata.creationDate < :from${queryId}`, //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
textParam);
}
} else {
q.where(
`media.metadata.creationDate < :to${queryId}`, //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
textParam
).andWhere(`media.metadata.creationDate >= :from${queryId}`, //TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
textParam);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) < :to${queryId}`,
textParam
).andWhere(`media.metadata.creationDate >= :from${queryId}`,
textParam);
} else {
}
}
} else {
@ -824,30 +845,54 @@ export class SearchManager {
if (Config.Database.type === DatabaseType.sqlite) {
//(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000))
if (tq.daysLength == 0) {
q.where(
//TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)`
);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`CAST(strftime('${duration}',(media.metadataCreationDate + (media.metadataCreationDateOffset * 60000))/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)`
);
} else {
q.where(
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)`
);
}
} else {
q.where(
//TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)`
)[whereFN](`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`,
textParam);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`CAST(strftime('${duration}',(media.metadataCreationDate + (media.metadataCreationDateOffset * 60000))/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)`
)[whereFN](`CAST(strftime('${duration}',(media.metadataCreationDate + (media.metadataCreationDateOffset * 60000))/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`,
textParam);
} else {
q.where(
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)`
)[whereFN](`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`,
textParam);
}
}
} else {
if (tq.daysLength == 0) {
q.where(
//TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`CAST(FROM_UNIXTIME((media.metadataCreationDate + (media.metadataCreationDateOffset * 60000))/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
);
} else {
q.where(
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
);
}
} else {
q.where(
//TODO: Offset: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
)[whereFN](`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`,
textParam);
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`CAST(FROM_UNIXTIME((media.metadataCreationDate + (media.metadataCreationDateOffset * 60000))/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
)[whereFN](`CAST(FROM_UNIXTIME((media.metadataCreationDate + (media.metadataCreationDateOffset * 60000))/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`,
textParam);
} else {
q.where(
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
)[whereFN](`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`,
textParam);
}
}
}
};

View File

@ -277,8 +277,8 @@ export class DiskManager {
directory.oldestMedia = Number.MIN_SAFE_INTEGER;
directory.media.forEach((m) => {
directory.youngestMedia = Math.min(m.metadata.creationDate, directory.youngestMedia); //TODO: Offset:
directory.oldestMedia = Math.max(m.metadata.creationDate, directory.oldestMedia);
directory.youngestMedia = Math.min(Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset), directory.youngestMedia);
directory.oldestMedia = Math.max(Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset), directory.oldestMedia);
}
);

View File

@ -70,7 +70,7 @@ export class EmailMessenger extends Messenger<{
(media[i].metadata as PhotoMetadata).positionData?.country :
((media[i].metadata as PhotoMetadata).positionData?.city ?
(media[i].metadata as PhotoMetadata).positionData?.city : '');
const caption = Utils.getFullYear(media[i].metadata.creationDate, media[i].metadata.creationDateOffset) + (location ? ', ' + location : ''); //TODO: Offset:
const caption = Utils.getFullYear(Utils.getTimeMS(media[i].metadata.creationDate, media[i].metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset), undefined) + (location ? ', ' + location : '');
attachments.push({
filename: media[i].name,
path: media[i].thumbnailPath,

View File

@ -33,7 +33,7 @@ export abstract class Messenger<C extends Record<string, unknown> = Record<strin
if (Array.isArray(input) && input.length > 0
&& (input as MediaDTO[])[0]?.name
&& (input as MediaDTO[])[0]?.directory
&& (input as MediaDTO[])[0]?.metadata?.creationDate) { //TODO: Offset:
&& (input as MediaDTO[])[0]?.metadata?.creationDate) {
const media = input as MediaDTOWithThPath[];
for (let i = 0; i < media.length; ++i) {
media[i].thumbnailPath = await this.getThumbnail(media[i]);

View File

@ -227,11 +227,20 @@ export class Utils {
}
//Get the MS of the creationDate, adjusted for the offset. Effectively getting the MS value as if the photo did not contain an offset.
//One can consider this "Local" time of the photo.
//One can consider this "Local" time of the photo. Starting point is UTC, as MetadataLoader loads timestamps with unknown timestamps as UTC.
static getLocalTimeMS(creationDate: number, creationDateOffset: string) {
const offsetMinutes = Utils.getOffsetMinutes(creationDateOffset);
return creationDate + (offsetMinutes ? (offsetMinutes * 60000) : 0);
}
//Like getLocalTimeMS, but only if localTime is true, otherwise just returns creationDate (global time)
static getTimeMS(creationDate: number, creationDateOffset: string, localTime: boolean) {
if (localTime) {
return Utils.getLocalTimeMS(creationDate, creationDateOffset);
} else {
return creationDate;
}
}
static isLeapYear(year: number) {
return (0 == year % 4) && (0 != year % 100) || (0 == year % 400)

View File

@ -1089,7 +1089,10 @@ export class ClientGalleryConfig {
},
description: $localize`If enabled, timestamp offsets are ignored, meaning that the local times of pictures are used for searching, sorting and grouping. If disabled, global time is used and pictures with no timestamp are assumed to be in UTC (offset +00:00).`
})
ignoreTimestampOffset: boolean = true;
//DEVELOPER NOTE: The Database model stores the timestamp (creationDate) as milliseconds since 1970-01-01 UTC (global time). And stores and offset (creationDateOffset) as minutes.
//Ignoring timestamp for the user is the opposite for the database. If the user wants to ignore the offset, we have to add the offset to the creationDate to give the user the right experience.
ignoreTimestampOffset: boolean = true;
@ConfigProperty({
tags: {

View File

@ -23,7 +23,7 @@
<div class="col-2 align-self-center">
{{media.metadata.fileSize | fileSize}}
</div>
<div class="col-3 align-self-center" [title]="media.metadata.creationDate"> <!-- //TODO: Offset: -->
<div class="col-3 align-self-center" [title]="media.metadata.creationDate">
{{ media.metadata.creationDate | date : 'longDate' : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }},
{{ media.metadata.creationDate | date : (media.metadata.creationDateOffset ? 'HH:mm:ss ZZZZZ' : 'HH:mm:ss') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }}
</div>

View File

@ -6,6 +6,7 @@ import {ContentService} from '../content.service';
import {mergeMap, Observable, shareReplay} from 'rxjs';
import {MDFilesFilterPipe} from '../../../pipes/MDFilesFilterPipe';
import {MDFileDTO} from '../../../../../common/entities/MDFileDTO';
import {Config} from '../../../../../common/config/public/Config';
@Injectable()
export class BlogService {
@ -28,7 +29,7 @@ export class BlogService {
let firstMedia = Number.MAX_SAFE_INTEGER;
if (content.mediaGroups.length > 0) {
firstMedia = content.mediaGroups[0].media.reduce((p, m) =>
Math.min(m.metadata.creationDate, p), Number.MAX_SAFE_INTEGER); //TODO: Offset:
Math.min(Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset), p), Number.MAX_SAFE_INTEGER);
}
const files = this.mdFilesFilterPipe.transform(content.metaFile)

View File

@ -3,6 +3,8 @@ import {BehaviorSubject, Observable} from 'rxjs';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {DirectoryContent} from '../contentLoader.service';
import {map, switchMap} from 'rxjs/operators';
import {Config} from '../../../../../common/config/public/Config';
import {Utils} from '../../../../../common/Utils';
export enum FilterRenderType {
enum = 1,
@ -159,11 +161,11 @@ export class FilterService {
}
const ret: { date: Date, endDate: Date, dateStr: string, count: number, max: number }[] = [];
const minDate = prefiltered.media.reduce(
(p, curr) => Math.min(p, curr.metadata.creationDate), //TODO: Offset:
(p, curr) => Math.min(p, Utils.getTimeMS(curr.metadata.creationDate, curr.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset)),
Number.MAX_VALUE - 1
);
const maxDate = prefiltered.media.reduce(
(p, curr) => Math.max(p, curr.metadata.creationDate), //TODO: Offset:
(p, curr) => Math.max(p, Utils.getTimeMS(curr.metadata.creationDate, curr.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset)),
Number.MIN_VALUE + 1
);
const diff = (maxDate - minDate) / 1000;
@ -205,7 +207,7 @@ export class FilterService {
const startMediaDate = new Date(floorDate(minDate));
prefiltered.media.forEach(m => {
const key = Math.floor((floorDate(m.metadata.creationDate) - startMediaDate.getTime()) / 1000 / usedDiv); //TODO: Offset:
const key = Math.floor((floorDate(Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset)) - startMediaDate.getTime()) / 1000 / usedDiv);
const getDate = (index: number) => {
let d: Date;
@ -273,16 +275,16 @@ export class FilterService {
if (c.media.length > 0) {
// Update date filter range
afilters.dateFilter.minDate = c.media.reduce(
(p, curr) => Math.min(p, curr.metadata.creationDate), //TODO: Offset:
(p, curr) => Math.min(p, Utils.getTimeMS(curr.metadata.creationDate, curr.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset)),
Number.MAX_VALUE - 1
);
afilters.dateFilter.maxDate = c.media.reduce(
(p, curr) => Math.max(p, curr.metadata.creationDate), //TODO: Offset:
(p, curr) => Math.max(p, Utils.getTimeMS(curr.metadata.creationDate, curr.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset)),
Number.MIN_VALUE + 1
);
// Add a few sec padding
afilters.dateFilter.minDate -= (afilters.dateFilter.minDate % 1000) + 1000;
afilters.dateFilter.maxDate += (afilters.dateFilter.maxDate % 1000) + 1000;
afilters.dateFilter.minDate -= ((afilters.dateFilter.minDate % 1000) + 1000);
afilters.dateFilter.maxDate += ((1000 - (afilters.dateFilter.maxDate % 1000)) + 1000);
if (afilters.dateFilter.minFilter === Number.MIN_VALUE) {
afilters.dateFilter.minFilter = afilters.dateFilter.minDate;
@ -294,8 +296,8 @@ export class FilterService {
// Apply Date filter
c.media = c.media.filter(
(m) =>
m.metadata.creationDate >= afilters.dateFilter.minFilter && //TODO: Offset:
m.metadata.creationDate <= afilters.dateFilter.maxFilter //TODO: Offset:
Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset) >= afilters.dateFilter.minFilter &&
Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset) <= afilters.dateFilter.maxFilter
);
} else {
afilters.dateFilter.minDate = Number.MIN_VALUE;

View File

@ -508,7 +508,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
case LightBoxTitleTexts.persons:
return m.metadata.faces?.map(f => f.name)?.join(', ');
case LightBoxTitleTexts.date:
return this.datePipe.transform(m.metadata.creationDate, 'longDate', m.metadata.creationDateOffset); //TODO: Offset:
return this.datePipe.transform(Utils.getTimeMS(m.metadata.creationDate, m.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset) , 'longDate', 'UTC');
case LightBoxTitleTexts.location:
if (!m.metadata.positionData) {
return '';

View File

@ -54,7 +54,7 @@
</div>
<div class="col-11">
<div class="details-main">
{{ media.metadata.creationDate | date: (isThisYear() ? 'MMMM d' : 'longDate') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }} <!-- //TODO: Offset: -->
{{ media.metadata.creationDate | date: (isThisYear() ? 'MMMM d' : 'longDate') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }}
</div>
<div class="details-sub text-secondary row">
<div class="col-12">{{ media.metadata.creationDate | date : (media.metadata.creationDateOffset ? 'EEEE, HH:mm:ss ZZZZZ' : 'EEEE, HH:mm:ss') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }}</div>

View File

@ -150,11 +150,7 @@ export class GallerySortingService {
break;
case SortByTypes.Date:
media.sort((a: PhotoDTO, b: PhotoDTO): number => {
if (Config.Gallery.ignoreTimestampOffset === true) {
return Utils.getLocalTimeMS(a.metadata.creationDate, a.metadata.creationDateOffset) - Utils.getLocalTimeMS(b.metadata.creationDate, b.metadata.creationDateOffset);
} else {
return a.metadata.creationDate - b.metadata.creationDate;
}
return Utils.getTimeMS(a.metadata.creationDate, a.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset) - Utils.getTimeMS(b.metadata.creationDate, b.metadata.creationDateOffset, Config.Gallery.ignoreTimestampOffset);
});
break;
case SortByTypes.Rating:
@ -204,7 +200,7 @@ export class GallerySortingService {
//Datepipe used this way, converts creationDate to date in local time.
return (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate', m.metadata.creationDateOffset ? m.metadata.creationDateOffset : 'UTC');
} else {
//Grouping with global time, requires a common timeframe. Here and now it is UTC. (Could be config option later) //TODO: Configurable Default Timezone
//Grouping with global time, requires a common timeframe.
return (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate', 'UTC');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,8 @@
{
"creationDate": 1713040680000,
"fileSize": 71525,
"size": {
"height": 1080,
"width": 1920
}
}

View File

@ -173,7 +173,7 @@ describe('Typeorm integration', () => {
const photos = await pr
.createQueryBuilder('media')
.orderBy('media.metadata.creationDate', 'ASC') //TODO: Offset:
.orderBy('media.metadata.creationDate', 'ASC') //TODO: Offset: Create a test where it is ".orderBy('media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)', 'ASC')" instead
.where('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + photo.metadata.positionData.city + '%'})
.innerJoinAndSelect('media.directory', 'directory')
.limit(10)
@ -195,7 +195,7 @@ describe('Typeorm integration', () => {
await pr.save(photo);
const photos = await pr
.createQueryBuilder('media')
.orderBy('media.metadata.creationDate', 'ASC') //TODO: Offset:
.orderBy('media.metadata.creationDate', 'ASC') //TODO: Offset: Create a test where it is ".orderBy('media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)', 'ASC')" instead
.where('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + city + '%'})
.innerJoinAndSelect('media.directory', 'directory')
.limit(10)