Compare commits
5 Commits
v1.28.3_41
...
v1.28.4_41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bef411056 | ||
|
|
e79e92c60f | ||
|
|
858ad43d3b | ||
|
|
5761765ea7 | ||
|
|
6abc733763 |
@@ -147,6 +147,7 @@ wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.en
|
|||||||
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
||||||
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
||||||
* [Optional] Populate Mapbox value to use reverse geocoding.
|
* [Optional] Populate Mapbox value to use reverse geocoding.
|
||||||
|
* [Optional] Populate `TZ` as your timezone, default is `Etc/UTC`.
|
||||||
|
|
||||||
### Step 3 - Start the containers
|
### Step 3 - Start the containers
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ REDIS_HOSTNAME=immich_redis
|
|||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# Log message level - [simple|verbose]
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
LOG_LEVEL=simple
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
@@ -63,4 +68,12 @@ MAPBOX_KEY=
|
|||||||
# Custom message on the login page, should be written in HTML form.
|
# Custom message on the login page, should be written in HTML form.
|
||||||
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||||
|
|
||||||
PUBLIC_LOGIN_PAGE_MESSAGE=
|
PUBLIC_LOGIN_PAGE_MESSAGE=
|
||||||
|
|
||||||
|
# For correctly display your local time zone on the web, you can set the time zone here.
|
||||||
|
# Should work fine by default value, however, in case of incorrect timezone in EXIF, this value
|
||||||
|
# should be set to the correct timezone.
|
||||||
|
# Command to get timezone:
|
||||||
|
# - Linux: curl -s http://ip-api.com/json/ | grep -oP '(?<=timezone":")(.*?)(?=")'
|
||||||
|
|
||||||
|
# TZ=Etc/UTC
|
||||||
@@ -47,6 +47,8 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
environment:
|
||||||
|
- PUBLIC_TZ=${TZ}
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { AlbumEntity } from '../../../../../libs/database/src/entities/album.ent
|
|||||||
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
|
||||||
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
|
||||||
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
||||||
|
import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity])],
|
imports: [TypeOrmModule.forFeature([AssetEntity, UserEntity, AlbumEntity, AssetAlbumEntity, UserAlbumEntity])],
|
||||||
@@ -18,6 +19,10 @@ import { AlbumRepository, ALBUM_REPOSITORY } from './album-repository';
|
|||||||
provide: ALBUM_REPOSITORY,
|
provide: ALBUM_REPOSITORY,
|
||||||
useClass: AlbumRepository,
|
useClass: AlbumRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ASSET_REPOSITORY,
|
||||||
|
useClass: AssetRepository,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AlbumModule {}
|
export class AlbumModule {}
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { AlbumEntity } from '@app/database/entities/album.entity';
|
import { AlbumEntity } from '@app/database/entities/album.entity';
|
||||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||||
|
import { IAssetRepository } from '../asset/asset-repository';
|
||||||
|
|
||||||
describe('Album service', () => {
|
describe('Album service', () => {
|
||||||
let sut: AlbumService;
|
let sut: AlbumService;
|
||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||||
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||||
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
const authUser: AuthUserDto = Object.freeze({
|
||||||
id: '1111',
|
id: '1111',
|
||||||
email: 'auth@test.com',
|
email: 'auth@test.com',
|
||||||
@@ -118,7 +121,22 @@ describe('Album service', () => {
|
|||||||
getListByAssetId: jest.fn(),
|
getListByAssetId: jest.fn(),
|
||||||
getCountByUserId: jest.fn(),
|
getCountByUserId: jest.fn(),
|
||||||
};
|
};
|
||||||
sut = new AlbumService(albumRepositoryMock);
|
|
||||||
|
assetRepositoryMock = {
|
||||||
|
create: jest.fn(),
|
||||||
|
getAllByUserId: jest.fn(),
|
||||||
|
getAllByDeviceId: jest.fn(),
|
||||||
|
getAssetCountByTimeBucket: jest.fn(),
|
||||||
|
getById: jest.fn(),
|
||||||
|
getDetectedObjectsByUserId: jest.fn(),
|
||||||
|
getLocationsByUserId: jest.fn(),
|
||||||
|
getSearchPropertiesByUserId: jest.fn(),
|
||||||
|
getAssetByTimeBucket: jest.fn(),
|
||||||
|
getAssetByChecksum: jest.fn(),
|
||||||
|
getAssetCountByUserId: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates album', async () => {
|
it('creates album', async () => {
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ import { GetAlbumsDto } from './dto/get-albums.dto';
|
|||||||
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
||||||
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
|
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
|
||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
|
import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumService {
|
export class AlbumService {
|
||||||
constructor(@Inject(ALBUM_REPOSITORY) private _albumRepository: IAlbumRepository) {}
|
constructor(
|
||||||
|
@Inject(ALBUM_REPOSITORY) private _albumRepository: IAlbumRepository,
|
||||||
|
@Inject(ASSET_REPOSITORY) private _assetRepository: IAssetRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
private async _getAlbum({
|
private async _getAlbum({
|
||||||
authUser,
|
authUser,
|
||||||
@@ -54,6 +58,11 @@ export class AlbumService {
|
|||||||
return albums.map(mapAlbumExcludeAssetInfo);
|
return albums.map(mapAlbumExcludeAssetInfo);
|
||||||
}
|
}
|
||||||
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
||||||
|
|
||||||
|
for (const album of albums) {
|
||||||
|
await this._checkValidThumbnail(album);
|
||||||
|
}
|
||||||
|
|
||||||
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
|
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,4 +132,18 @@ export class AlbumService {
|
|||||||
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
async getAlbumCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
||||||
return this._albumRepository.getCountByUserId(authUser.id);
|
return this._albumRepository.getCountByUserId(authUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _checkValidThumbnail(album: AlbumEntity): Promise<AlbumEntity> {
|
||||||
|
const assetId = album.albumThumbnailAssetId;
|
||||||
|
if (assetId) {
|
||||||
|
try {
|
||||||
|
await this._assetRepository.getById(assetId);
|
||||||
|
} catch (e) {
|
||||||
|
album.albumThumbnailAssetId = null;
|
||||||
|
return await this._albumRepository.updateAlbum(album, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return album;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
|
import { timeUtils } from '@app/common/utils';
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@@ -56,6 +57,18 @@ export class AssetService {
|
|||||||
mimeType: string,
|
mimeType: string,
|
||||||
checksum: Buffer,
|
checksum: Buffer,
|
||||||
): Promise<AssetEntity> {
|
): Promise<AssetEntity> {
|
||||||
|
// Check valid time.
|
||||||
|
const createdAt = createAssetDto.createdAt;
|
||||||
|
const modifiedAt = createAssetDto.modifiedAt;
|
||||||
|
|
||||||
|
if (!timeUtils.checkValidTimestamp(createdAt)) {
|
||||||
|
createAssetDto.createdAt = await timeUtils.getTimestampFromExif(originalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeUtils.checkValidTimestamp(modifiedAt)) {
|
||||||
|
createAssetDto.modifiedAt = await timeUtils.getTimestampFromExif(originalPath);
|
||||||
|
}
|
||||||
|
|
||||||
const assetEntity = await this._assetRepository.create(
|
const assetEntity = await this._assetRepository.create(
|
||||||
createAssetDto,
|
createAssetDto,
|
||||||
authUser.id,
|
authUser.id,
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export interface IServerVersion {
|
|||||||
export const serverVersion: IServerVersion = {
|
export const serverVersion: IServerVersion = {
|
||||||
major: 1,
|
major: 1,
|
||||||
minor: 28,
|
minor: 28,
|
||||||
patch: 3,
|
patch: 4,
|
||||||
build: 41,
|
build: 41,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from '@app/job/constants/queue-name.constant';
|
} from '@app/job/constants/queue-name.constant';
|
||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
|
import { CommunicationModule } from '../../immich/src/api-v1/communication/communication.module';
|
||||||
import { MicroservicesService } from './microservices.service';
|
import { MicroservicesService } from './microservices.service';
|
||||||
@@ -40,42 +40,48 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue(
|
||||||
name: thumbnailGeneratorQueueName,
|
{
|
||||||
defaultJobOptions: {
|
name: thumbnailGeneratorQueueName,
|
||||||
attempts: 3,
|
defaultJobOptions: {
|
||||||
removeOnComplete: true,
|
attempts: 3,
|
||||||
removeOnFail: false,
|
removeOnComplete: true,
|
||||||
|
removeOnFail: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
name: assetUploadedQueueName,
|
name: assetUploadedQueueName,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
name: metadataExtractionQueueName,
|
name: metadataExtractionQueueName,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
name: videoConversionQueueName,
|
name: videoConversionQueueName,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
{
|
||||||
name: generateChecksumQueueName,
|
name: generateChecksumQueueName,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
),
|
||||||
CommunicationModule,
|
CommunicationModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
@@ -86,6 +92,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
|
|||||||
MetadataExtractionProcessor,
|
MetadataExtractionProcessor,
|
||||||
VideoTranscodeProcessor,
|
VideoTranscodeProcessor,
|
||||||
GenerateChecksumProcessor,
|
GenerateChecksumProcessor,
|
||||||
|
ConfigService,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ImmichLogLevel } from '@app/common/constants/log-level.constant';
|
||||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||||
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
import { SmartInfoEntity } from '@app/database/entities/smart-info.entity';
|
||||||
@@ -16,6 +17,7 @@ import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
|
|||||||
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
@@ -28,6 +30,7 @@ import { Repository } from 'typeorm/repository/Repository';
|
|||||||
@Processor(metadataExtractionQueueName)
|
@Processor(metadataExtractionQueueName)
|
||||||
export class MetadataExtractionProcessor {
|
export class MetadataExtractionProcessor {
|
||||||
private geocodingClient?: GeocodeService;
|
private geocodingClient?: GeocodeService;
|
||||||
|
private logLevel: ImmichLogLevel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
@@ -38,12 +41,16 @@ export class MetadataExtractionProcessor {
|
|||||||
|
|
||||||
@InjectRepository(SmartInfoEntity)
|
@InjectRepository(SmartInfoEntity)
|
||||||
private smartInfoRepository: Repository<SmartInfoEntity>,
|
private smartInfoRepository: Repository<SmartInfoEntity>,
|
||||||
|
|
||||||
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) {
|
if (process.env.ENABLE_MAPBOX == 'true' && process.env.MAPBOX_KEY) {
|
||||||
this.geocodingClient = mapboxGeocoding({
|
this.geocodingClient = mapboxGeocoding({
|
||||||
accessToken: process.env.MAPBOX_KEY,
|
accessToken: process.env.MAPBOX_KEY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process(exifExtractionProcessorName)
|
@Process(exifExtractionProcessorName)
|
||||||
@@ -139,6 +146,10 @@ export class MetadataExtractionProcessor {
|
|||||||
await this.exifRepository.save(newExif);
|
await this.exifRepository.save(newExif);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error(`Error extracting EXIF ${String(e)}`, 'extractExif');
|
Logger.error(`Error extracting EXIF ${String(e)}`, 'extractExif');
|
||||||
|
|
||||||
|
if (this.logLevel === ImmichLogLevel.VERBOSE) {
|
||||||
|
console.trace('Error extracting EXIF', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ImmichLogLevel } from '@app/common/constants/log-level.constant';
|
||||||
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
|
||||||
import {
|
import {
|
||||||
WebpGeneratorProcessor,
|
WebpGeneratorProcessor,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
} from '@app/job';
|
} from '@app/job';
|
||||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { mapAsset } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
|
import { mapAsset } from 'apps/immich/src/api-v1/asset/response-dto/asset-response.dto';
|
||||||
import { Job, Queue } from 'bull';
|
import { Job, Queue } from 'bull';
|
||||||
@@ -23,6 +25,8 @@ import { CommunicationGateway } from '../../../immich/src/api-v1/communication/c
|
|||||||
|
|
||||||
@Processor(thumbnailGeneratorQueueName)
|
@Processor(thumbnailGeneratorQueueName)
|
||||||
export class ThumbnailGeneratorProcessor {
|
export class ThumbnailGeneratorProcessor {
|
||||||
|
private logLevel: ImmichLogLevel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
@@ -34,7 +38,11 @@ export class ThumbnailGeneratorProcessor {
|
|||||||
|
|
||||||
@InjectQueue(metadataExtractionQueueName)
|
@InjectQueue(metadataExtractionQueueName)
|
||||||
private metadataExtractionQueue: Queue,
|
private metadataExtractionQueue: Queue,
|
||||||
) {}
|
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.logLevel = this.configService.get('LOG_LEVEL') || ImmichLogLevel.SIMPLE;
|
||||||
|
}
|
||||||
|
|
||||||
@Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
|
@Process({ name: generateJPEGThumbnailProcessorName, concurrency: 3 })
|
||||||
async generateJPEGThumbnail(job: Job<JpegGeneratorProcessor>) {
|
async generateJPEGThumbnail(job: Job<JpegGeneratorProcessor>) {
|
||||||
@@ -51,8 +59,16 @@ export class ThumbnailGeneratorProcessor {
|
|||||||
const jpegThumbnailPath = resizePath + originalFilename + '.jpeg';
|
const jpegThumbnailPath = resizePath + originalFilename + '.jpeg';
|
||||||
|
|
||||||
if (asset.type == AssetType.IMAGE) {
|
if (asset.type == AssetType.IMAGE) {
|
||||||
await sharp(asset.originalPath).resize(1440, 2560, { fit: 'inside' }).jpeg().rotate().toFile(jpegThumbnailPath);
|
try {
|
||||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
await sharp(asset.originalPath).resize(1440, 2560, { fit: 'inside' }).jpeg().rotate().toFile(jpegThumbnailPath);
|
||||||
|
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id);
|
||||||
|
|
||||||
|
if (this.logLevel == ImmichLogLevel.VERBOSE) {
|
||||||
|
console.trace('Failed to generate jpeg thumbnail for asset', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update resize path to send to generate webp queue
|
// Update resize path to send to generate webp queue
|
||||||
asset.resizePath = jpegThumbnailPath;
|
asset.resizePath = jpegThumbnailPath;
|
||||||
@@ -105,7 +121,15 @@ export class ThumbnailGeneratorProcessor {
|
|||||||
|
|
||||||
const webpPath = asset.resizePath.replace('jpeg', 'webp');
|
const webpPath = asset.resizePath.replace('jpeg', 'webp');
|
||||||
|
|
||||||
await sharp(asset.resizePath).resize(250).webp().rotate().toFile(webpPath);
|
try {
|
||||||
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
|
await sharp(asset.resizePath).resize(250).webp().rotate().toFile(webpPath);
|
||||||
|
await this.assetRepository.update({ id: asset.id }, { webpPath: webpPath });
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('Failed to generate webp thumbnail for asset: ' + asset.id);
|
||||||
|
|
||||||
|
if (this.logLevel == ImmichLogLevel.VERBOSE) {
|
||||||
|
console.trace('Failed to generate webp thumbnail for asset', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ export const immichAppConfig: ConfigModuleOptions = {
|
|||||||
then: Joi.string().optional().allow(null, ''),
|
then: Joi.string().optional().allow(null, ''),
|
||||||
otherwise: Joi.string().required(),
|
otherwise: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
|
LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
4
server/libs/common/src/constants/log-level.constant.ts
Normal file
4
server/libs/common/src/constants/log-level.constant.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum ImmichLogLevel {
|
||||||
|
SIMPLE = 'simple',
|
||||||
|
VERBOSE = 'verbose',
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './config';
|
export * from './config';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
export * from './utils';
|
||||||
|
|||||||
1
server/libs/common/src/utils/index.ts
Normal file
1
server/libs/common/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './time-utils';
|
||||||
37
server/libs/common/src/utils/time-utils.spec.ts
Normal file
37
server/libs/common/src/utils/time-utils.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// create unit test for time utils
|
||||||
|
|
||||||
|
import { timeUtils } from './time-utils';
|
||||||
|
|
||||||
|
describe('Time Utilities', () => {
|
||||||
|
describe('checkValidTimestamp', () => {
|
||||||
|
it('check for year 0000', () => {
|
||||||
|
const result = timeUtils.checkValidTimestamp('0000-00-00T00:00:00.000Z');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check for 6-digits year with plus sign', () => {
|
||||||
|
const result = timeUtils.checkValidTimestamp('+12345-00-00T00:00:00.000Z');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check for 6-digits year with negative sign', () => {
|
||||||
|
const result = timeUtils.checkValidTimestamp('-12345-00-00T00:00:00.000Z');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check for current date', () => {
|
||||||
|
const result = timeUtils.checkValidTimestamp(new Date().toISOString());
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check for year before 1583', () => {
|
||||||
|
const result = timeUtils.checkValidTimestamp('1582-12-31T23:59:59.999Z');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('check for year after 9999', () => {
|
||||||
|
const result = timeUtils.checkValidTimestamp('10000-00-00T00:00:00.000Z');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
48
server/libs/common/src/utils/time-utils.ts
Normal file
48
server/libs/common/src/utils/time-utils.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import exifr from 'exifr';
|
||||||
|
|
||||||
|
function createTimeUtils() {
|
||||||
|
const checkValidTimestamp = (timestamp: string): boolean => {
|
||||||
|
const parsedTimestamp = Date.parse(timestamp);
|
||||||
|
|
||||||
|
if (isNaN(parsedTimestamp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(parsedTimestamp);
|
||||||
|
|
||||||
|
if (date.getFullYear() < 1583 || date.getFullYear() > 9999) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.getFullYear() > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimestampFromExif = async (originalPath: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const exifData = await exifr.parse(originalPath, {
|
||||||
|
tiff: true,
|
||||||
|
ifd0: true as any,
|
||||||
|
ifd1: true,
|
||||||
|
exif: true,
|
||||||
|
gps: true,
|
||||||
|
interop: true,
|
||||||
|
xmp: true,
|
||||||
|
icc: true,
|
||||||
|
iptc: true,
|
||||||
|
jfif: true,
|
||||||
|
ihdr: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exifData && exifData['DateTimeOriginal']) {
|
||||||
|
return exifData['DateTimeOriginal'];
|
||||||
|
} else {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { checkValidTimestamp, getTimestampFromExif };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const timeUtils = createTimeUtils();
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
"^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
|
"^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
|
||||||
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
|
"@app/database/config/(.*)": "<rootDir>/libs/database/src/config/$1",
|
||||||
"@app/database/config": "<rootDir>/libs/database/src/config",
|
"@app/database/config": "<rootDir>/libs/database/src/config",
|
||||||
|
"@app/common": "<rootDir>/libs/common/src",
|
||||||
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
|
"^@app/job(|/.*)$": "<rootDir>/libs/job/src/$1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { browser } from '$app/env';
|
import { browser } from '$app/env';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
import { AssetResponseDto, AlbumResponseDto } from '@api';
|
import { AssetResponseDto, AlbumResponseDto } from '@api';
|
||||||
|
|
||||||
type Leaflet = typeof import('leaflet');
|
type Leaflet = typeof import('leaflet');
|
||||||
@@ -30,6 +31,13 @@
|
|||||||
if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
|
if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
|
||||||
await drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
|
await drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove timezone when user not config PUBLIC_TZ var. Etc/UTC is used in default.
|
||||||
|
if (asset.exifInfo?.dateTimeOriginal && !env.PUBLIC_TZ) {
|
||||||
|
const dateTimeOriginal = asset.exifInfo.dateTimeOriginal;
|
||||||
|
|
||||||
|
asset.exifInfo.dateTimeOriginal = dateTimeOriginal.slice(0, dateTimeOriginal.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,11 +134,7 @@
|
|||||||
<p>{moment(asset.exifInfo.dateTimeOriginal).format('MMM DD, YYYY')}</p>
|
<p>{moment(asset.exifInfo.dateTimeOriginal).format('MMM DD, YYYY')}</p>
|
||||||
<div class="flex gap-2 text-sm">
|
<div class="flex gap-2 text-sm">
|
||||||
<p>
|
<p>
|
||||||
{moment(
|
{moment(asset.exifInfo.dateTimeOriginal).format('ddd, hh:mm A')}
|
||||||
asset.exifInfo.dateTimeOriginal
|
|
||||||
.toString()
|
|
||||||
.slice(0, asset.exifInfo.dateTimeOriginal.toString().length - 1)
|
|
||||||
).format('ddd, hh:mm A')}
|
|
||||||
</p>
|
</p>
|
||||||
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
|
<p>GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user