mirror of
https://github.com/immich-app/immich.git
synced 2025-03-01 15:11:21 +01:00
Add timezone to exif entity (#1894)
* Add timezone to exif entity * Refactor logging --------- Co-authored-by: Andrea Alemani <andrea.alemani94@gmail.com>
This commit is contained in:
parent
8b001b87d2
commit
94b2ea9b5f
11 changed files with 93 additions and 13 deletions
1
mobile/openapi/doc/ExifResponseDto.md
generated
1
mobile/openapi/doc/ExifResponseDto.md
generated
|
@ -17,6 +17,7 @@ Name | Type | Description | Notes
|
||||||
**orientation** | **String** | | [optional]
|
**orientation** | **String** | | [optional]
|
||||||
**dateTimeOriginal** | [**DateTime**](DateTime.md) | | [optional]
|
**dateTimeOriginal** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
**modifyDate** | [**DateTime**](DateTime.md) | | [optional]
|
**modifyDate** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
|
**timeZone** | **String** | | [optional]
|
||||||
**lensModel** | **String** | | [optional]
|
**lensModel** | **String** | | [optional]
|
||||||
**fNumber** | **num** | | [optional]
|
**fNumber** | **num** | | [optional]
|
||||||
**focalLength** | **num** | | [optional]
|
**focalLength** | **num** | | [optional]
|
||||||
|
|
13
mobile/openapi/lib/model/exif_response_dto.dart
generated
13
mobile/openapi/lib/model/exif_response_dto.dart
generated
|
@ -22,6 +22,7 @@ class ExifResponseDto {
|
||||||
this.orientation,
|
this.orientation,
|
||||||
this.dateTimeOriginal,
|
this.dateTimeOriginal,
|
||||||
this.modifyDate,
|
this.modifyDate,
|
||||||
|
this.timeZone,
|
||||||
this.lensModel,
|
this.lensModel,
|
||||||
this.fNumber,
|
this.fNumber,
|
||||||
this.focalLength,
|
this.focalLength,
|
||||||
|
@ -52,6 +53,8 @@ class ExifResponseDto {
|
||||||
|
|
||||||
DateTime? modifyDate;
|
DateTime? modifyDate;
|
||||||
|
|
||||||
|
String? timeZone;
|
||||||
|
|
||||||
String? lensModel;
|
String? lensModel;
|
||||||
|
|
||||||
num? fNumber;
|
num? fNumber;
|
||||||
|
@ -83,6 +86,7 @@ class ExifResponseDto {
|
||||||
other.orientation == orientation &&
|
other.orientation == orientation &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
other.modifyDate == modifyDate &&
|
other.modifyDate == modifyDate &&
|
||||||
|
other.timeZone == timeZone &&
|
||||||
other.lensModel == lensModel &&
|
other.lensModel == lensModel &&
|
||||||
other.fNumber == fNumber &&
|
other.fNumber == fNumber &&
|
||||||
other.focalLength == focalLength &&
|
other.focalLength == focalLength &&
|
||||||
|
@ -106,6 +110,7 @@ class ExifResponseDto {
|
||||||
(orientation == null ? 0 : orientation!.hashCode) +
|
(orientation == null ? 0 : orientation!.hashCode) +
|
||||||
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
||||||
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
||||||
|
(timeZone == null ? 0 : timeZone!.hashCode) +
|
||||||
(lensModel == null ? 0 : lensModel!.hashCode) +
|
(lensModel == null ? 0 : lensModel!.hashCode) +
|
||||||
(fNumber == null ? 0 : fNumber!.hashCode) +
|
(fNumber == null ? 0 : fNumber!.hashCode) +
|
||||||
(focalLength == null ? 0 : focalLength!.hashCode) +
|
(focalLength == null ? 0 : focalLength!.hashCode) +
|
||||||
|
@ -118,7 +123,7 @@ class ExifResponseDto {
|
||||||
(country == null ? 0 : country!.hashCode);
|
(country == null ? 0 : country!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, imageName=$imageName, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]';
|
String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, imageName=$imageName, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -167,6 +172,11 @@ class ExifResponseDto {
|
||||||
} else {
|
} else {
|
||||||
// json[r'modifyDate'] = null;
|
// json[r'modifyDate'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.timeZone != null) {
|
||||||
|
json[r'timeZone'] = this.timeZone;
|
||||||
|
} else {
|
||||||
|
// json[r'timeZone'] = null;
|
||||||
|
}
|
||||||
if (this.lensModel != null) {
|
if (this.lensModel != null) {
|
||||||
json[r'lensModel'] = this.lensModel;
|
json[r'lensModel'] = this.lensModel;
|
||||||
} else {
|
} else {
|
||||||
|
@ -252,6 +262,7 @@ class ExifResponseDto {
|
||||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', ''),
|
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', ''),
|
||||||
modifyDate: mapDateTime(json, r'modifyDate', ''),
|
modifyDate: mapDateTime(json, r'modifyDate', ''),
|
||||||
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
||||||
fNumber: json[r'fNumber'] == null
|
fNumber: json[r'fNumber'] == null
|
||||||
? null
|
? null
|
||||||
|
|
5
mobile/openapi/test/exif_response_dto_test.dart
generated
5
mobile/openapi/test/exif_response_dto_test.dart
generated
|
@ -61,6 +61,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String timeZone
|
||||||
|
test('to test the property `timeZone`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String lensModel
|
// String lensModel
|
||||||
test('to test the property `lensModel`', () async {
|
test('to test the property `lensModel`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
|
import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
|
||||||
|
import tz_lookup from '@photostructure/tz-lookup';
|
||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||||
import { getName } from 'i18n-iso-countries';
|
import { getName } from 'i18n-iso-countries';
|
||||||
import geocoder, { InitOptions } from 'local-reverse-geocoder';
|
import geocoder, { InitOptions } from 'local-reverse-geocoder';
|
||||||
|
@ -190,6 +191,17 @@ export class MetadataExtractionProcessor {
|
||||||
return exifDate.toDate();
|
return exifDate.toDate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exifTimeZone = (exifDate: string | ExifDateTime | undefined) => {
|
||||||
|
if (!exifDate) return null;
|
||||||
|
|
||||||
|
if (typeof exifDate === 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exifDate.zone ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeZone = exifTimeZone(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.fileCreatedAt);
|
||||||
const fileCreatedAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.fileCreatedAt);
|
const fileCreatedAt = exifToDate(exifData?.DateTimeOriginal ?? exifData?.CreateDate ?? asset.fileCreatedAt);
|
||||||
const fileModifiedAt = exifToDate(exifData?.ModifyDate ?? asset.fileModifiedAt);
|
const fileModifiedAt = exifToDate(exifData?.ModifyDate ?? asset.fileModifiedAt);
|
||||||
const fileStats = fs.statSync(asset.originalPath);
|
const fileStats = fs.statSync(asset.originalPath);
|
||||||
|
@ -207,6 +219,7 @@ export class MetadataExtractionProcessor {
|
||||||
newExif.orientation = exifData?.Orientation?.toString() || null;
|
newExif.orientation = exifData?.Orientation?.toString() || null;
|
||||||
newExif.dateTimeOriginal = fileCreatedAt;
|
newExif.dateTimeOriginal = fileCreatedAt;
|
||||||
newExif.modifyDate = fileModifiedAt;
|
newExif.modifyDate = fileModifiedAt;
|
||||||
|
newExif.timeZone = timeZone;
|
||||||
newExif.lensModel = exifData?.LensModel || null;
|
newExif.lensModel = exifData?.LensModel || null;
|
||||||
newExif.fNumber = exifData?.FNumber || null;
|
newExif.fNumber = exifData?.FNumber || null;
|
||||||
newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null;
|
newExif.focalLength = exifData?.FocalLength ? parseFloat(exifData.FocalLength) : null;
|
||||||
|
@ -308,6 +321,7 @@ export class MetadataExtractionProcessor {
|
||||||
newExif.fileSizeInByte = data.format.size || null;
|
newExif.fileSizeInByte = data.format.size || null;
|
||||||
newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
|
newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null;
|
||||||
newExif.modifyDate = null;
|
newExif.modifyDate = null;
|
||||||
|
newExif.timeZone = null;
|
||||||
newExif.latitude = null;
|
newExif.latitude = null;
|
||||||
newExif.longitude = null;
|
newExif.longitude = null;
|
||||||
newExif.city = null;
|
newExif.city = null;
|
||||||
|
@ -345,6 +359,14 @@ export class MetadataExtractionProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newExif.longitude && newExif.latitude) {
|
||||||
|
try {
|
||||||
|
newExif.timeZone = tz_lookup(newExif.latitude, newExif.longitude);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`Error while calculating timezone from gps coordinates: ${error}`, error?.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reverse GeoCoding
|
// Reverse GeoCoding
|
||||||
if (this.isGeocodeInitialized && newExif.longitude && newExif.latitude) {
|
if (this.isGeocodeInitialized && newExif.longitude && newExif.latitude) {
|
||||||
const { country, state, city } = await this.reverseGeocodeExif(newExif.latitude, newExif.longitude);
|
const { country, state, city } = await this.reverseGeocodeExif(newExif.latitude, newExif.longitude);
|
||||||
|
|
|
@ -3537,6 +3537,11 @@
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"default": null
|
"default": null
|
||||||
},
|
},
|
||||||
|
"timeZone": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"default": null
|
||||||
|
},
|
||||||
"lensModel": {
|
"lensModel": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
|
|
|
@ -13,6 +13,7 @@ export class ExifResponseDto {
|
||||||
orientation?: string | null = null;
|
orientation?: string | null = null;
|
||||||
dateTimeOriginal?: Date | null = null;
|
dateTimeOriginal?: Date | null = null;
|
||||||
modifyDate?: Date | null = null;
|
modifyDate?: Date | null = null;
|
||||||
|
timeZone?: string | null = null;
|
||||||
lensModel?: string | null = null;
|
lensModel?: string | null = null;
|
||||||
fNumber?: number | null = null;
|
fNumber?: number | null = null;
|
||||||
focalLength?: number | null = null;
|
focalLength?: number | null = null;
|
||||||
|
@ -36,6 +37,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||||
orientation: entity.orientation,
|
orientation: entity.orientation,
|
||||||
dateTimeOriginal: entity.dateTimeOriginal,
|
dateTimeOriginal: entity.dateTimeOriginal,
|
||||||
modifyDate: entity.modifyDate,
|
modifyDate: entity.modifyDate,
|
||||||
|
timeZone: entity.timeZone,
|
||||||
lensModel: entity.lensModel,
|
lensModel: entity.lensModel,
|
||||||
fNumber: entity.fNumber,
|
fNumber: entity.fNumber,
|
||||||
focalLength: entity.focalLength,
|
focalLength: entity.focalLength,
|
||||||
|
|
|
@ -323,6 +323,7 @@ const assetInfo: ExifResponseDto = {
|
||||||
orientation: 'orientation',
|
orientation: 'orientation',
|
||||||
dateTimeOriginal: today,
|
dateTimeOriginal: today,
|
||||||
modifyDate: today,
|
modifyDate: today,
|
||||||
|
timeZone: 'America/Los_Angeles',
|
||||||
lensModel: 'fancy',
|
lensModel: 'fancy',
|
||||||
fNumber: 100,
|
fNumber: 100,
|
||||||
focalLength: 100,
|
focalLength: 100,
|
||||||
|
@ -607,6 +608,7 @@ export const sharedLinkStub = {
|
||||||
orientation: 'orientation',
|
orientation: 'orientation',
|
||||||
dateTimeOriginal: today,
|
dateTimeOriginal: today,
|
||||||
modifyDate: today,
|
modifyDate: today,
|
||||||
|
timeZone: 'America/Los_Angeles',
|
||||||
latitude: 100,
|
latitude: 100,
|
||||||
longitude: 100,
|
longitude: 100,
|
||||||
city: 'city',
|
city: 'city',
|
||||||
|
|
|
@ -34,6 +34,9 @@ export class ExifEntity {
|
||||||
@Column({ type: 'timestamptz', nullable: true })
|
@Column({ type: 'timestamptz', nullable: true })
|
||||||
modifyDate!: Date | null;
|
modifyDate!: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
timeZone!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'float', nullable: true })
|
@Column({ type: 'float', nullable: true })
|
||||||
latitude!: number | null;
|
latitude!: number | null;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddExifTimeZone1677497925328 implements MigrationInterface {
|
||||||
|
name = 'AddExifTimeZone1677497925328'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" ADD "timeZone" character varying`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "timeZone"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
web/src/api/open-api/api.ts
generated
6
web/src/api/open-api/api.ts
generated
|
@ -1126,6 +1126,12 @@ export interface ExifResponseDto {
|
||||||
* @memberof ExifResponseDto
|
* @memberof ExifResponseDto
|
||||||
*/
|
*/
|
||||||
'modifyDate'?: string | null;
|
'modifyDate'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ExifResponseDto
|
||||||
|
*/
|
||||||
|
'timeZone'?: string | null;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import { AssetResponseDto, AlbumResponseDto } from '@api';
|
import { AssetResponseDto, AlbumResponseDto } from '@api';
|
||||||
import { asByteUnitString } from '../../utils/byte-units';
|
import { asByteUnitString } from '../../utils/byte-units';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import type { LatLngTuple } from 'leaflet';
|
import type { LatLngTuple } from 'leaflet';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
|
@ -55,7 +56,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if asset.exifInfo?.dateTimeOriginal}
|
{#if asset.exifInfo?.dateTimeOriginal}
|
||||||
{@const assetDateTimeOriginal = new Date(asset.exifInfo.dateTimeOriginal)}
|
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
|
||||||
|
zone: asset.exifInfo.timeZone ?? undefined
|
||||||
|
})}
|
||||||
<div class="flex gap-4 py-4">
|
<div class="flex gap-4 py-4">
|
||||||
<div>
|
<div>
|
||||||
<Calendar size="24" />
|
<Calendar size="24" />
|
||||||
|
@ -63,20 +66,26 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
{assetDateTimeOriginal.toLocaleDateString($locale, {
|
{assetDateTimeOriginal.toLocaleString(
|
||||||
month: 'short',
|
{
|
||||||
day: 'numeric',
|
month: 'short',
|
||||||
year: 'numeric'
|
day: 'numeric',
|
||||||
})}
|
year: 'numeric'
|
||||||
|
},
|
||||||
|
{ locale: $locale }
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2 text-sm">
|
<div class="flex gap-2 text-sm">
|
||||||
<p>
|
<p>
|
||||||
{assetDateTimeOriginal.toLocaleString($locale, {
|
{assetDateTimeOriginal.toLocaleString(
|
||||||
weekday: 'short',
|
{
|
||||||
hour: 'numeric',
|
weekday: 'short',
|
||||||
minute: '2-digit',
|
hour: 'numeric',
|
||||||
timeZoneName: 'longOffset'
|
minute: '2-digit',
|
||||||
})}
|
timeZoneName: 'longOffset'
|
||||||
|
},
|
||||||
|
{ locale: $locale }
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue