mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 16:41:59 +00:00
chore(server): Store generated files (thumbnails, encoded video) in subdirectories (#4112)
* save thumbnails in subdirectories * migration job, migrate assets and face thumbnails * fix tests * directory depth of two instead of three * cleanup empty dirs after migration * clean up empty dirs after migration, migrate people without assetId * add job card for new migration job * fix removeEmptyDirs race condition because of missing await * cleanup empty directories after asset deletion * move ensurePath to storage core * rename jobs * remove unnecessary property of IEntityJob * use updated person getById, minor refactoring * ensure that directory cleanup doesn't interfere with migration * better description for job in ui * fix remove directories when migration is done * cleanup empty folders at start of migration * fix: actually persist concurrency setting * add comment explaining regex * chore: cleanup --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
07069c3b1e
commit
3053cbd4c8
36 changed files with 277 additions and 100 deletions
13
cli/src/api/open-api/api.ts
generated
13
cli/src/api/open-api/api.ts
generated
|
@ -307,6 +307,12 @@ export interface AllJobStatusResponseDto {
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'metadataExtraction': JobStatusDto;
|
'metadataExtraction': JobStatusDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {JobStatusDto}
|
||||||
|
* @memberof AllJobStatusResponseDto
|
||||||
|
*/
|
||||||
|
'migration': JobStatusDto;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobStatusDto}
|
* @type {JobStatusDto}
|
||||||
|
@ -1779,6 +1785,7 @@ export const JobName = {
|
||||||
ClipEncoding: 'clipEncoding',
|
ClipEncoding: 'clipEncoding',
|
||||||
BackgroundTask: 'backgroundTask',
|
BackgroundTask: 'backgroundTask',
|
||||||
StorageTemplateMigration: 'storageTemplateMigration',
|
StorageTemplateMigration: 'storageTemplateMigration',
|
||||||
|
Migration: 'migration',
|
||||||
Search: 'search',
|
Search: 'search',
|
||||||
Sidecar: 'sidecar',
|
Sidecar: 'sidecar',
|
||||||
Library: 'library'
|
Library: 'library'
|
||||||
|
@ -3240,6 +3247,12 @@ export interface SystemConfigJobDto {
|
||||||
* @memberof SystemConfigJobDto
|
* @memberof SystemConfigJobDto
|
||||||
*/
|
*/
|
||||||
'metadataExtraction': JobSettingsDto;
|
'metadataExtraction': JobSettingsDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {JobSettingsDto}
|
||||||
|
* @memberof SystemConfigJobDto
|
||||||
|
*/
|
||||||
|
'migration': JobSettingsDto;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobSettingsDto}
|
* @type {JobSettingsDto}
|
||||||
|
|
BIN
mobile/openapi/doc/AllJobStatusResponseDto.md
generated
BIN
mobile/openapi/doc/AllJobStatusResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigJobDto.md
generated
BIN
mobile/openapi/doc/SystemConfigJobDto.md
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/job_name.dart
generated
BIN
mobile/openapi/lib/model/job_name.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_job_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_job_dto.dart
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/test/system_config_job_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_job_dto_test.dart
generated
Binary file not shown.
|
@ -5343,6 +5343,9 @@
|
||||||
"metadataExtraction": {
|
"metadataExtraction": {
|
||||||
"$ref": "#/components/schemas/JobStatusDto"
|
"$ref": "#/components/schemas/JobStatusDto"
|
||||||
},
|
},
|
||||||
|
"migration": {
|
||||||
|
"$ref": "#/components/schemas/JobStatusDto"
|
||||||
|
},
|
||||||
"objectTagging": {
|
"objectTagging": {
|
||||||
"$ref": "#/components/schemas/JobStatusDto"
|
"$ref": "#/components/schemas/JobStatusDto"
|
||||||
},
|
},
|
||||||
|
@ -5372,6 +5375,7 @@
|
||||||
"objectTagging",
|
"objectTagging",
|
||||||
"clipEncoding",
|
"clipEncoding",
|
||||||
"storageTemplateMigration",
|
"storageTemplateMigration",
|
||||||
|
"migration",
|
||||||
"backgroundTask",
|
"backgroundTask",
|
||||||
"search",
|
"search",
|
||||||
"recognizeFaces",
|
"recognizeFaces",
|
||||||
|
@ -6535,6 +6539,7 @@
|
||||||
"clipEncoding",
|
"clipEncoding",
|
||||||
"backgroundTask",
|
"backgroundTask",
|
||||||
"storageTemplateMigration",
|
"storageTemplateMigration",
|
||||||
|
"migration",
|
||||||
"search",
|
"search",
|
||||||
"sidecar",
|
"sidecar",
|
||||||
"library"
|
"library"
|
||||||
|
@ -7693,6 +7698,9 @@
|
||||||
"metadataExtraction": {
|
"metadataExtraction": {
|
||||||
"$ref": "#/components/schemas/JobSettingsDto"
|
"$ref": "#/components/schemas/JobSettingsDto"
|
||||||
},
|
},
|
||||||
|
"migration": {
|
||||||
|
"$ref": "#/components/schemas/JobSettingsDto"
|
||||||
|
},
|
||||||
"objectTagging": {
|
"objectTagging": {
|
||||||
"$ref": "#/components/schemas/JobSettingsDto"
|
"$ref": "#/components/schemas/JobSettingsDto"
|
||||||
},
|
},
|
||||||
|
@ -7722,6 +7730,7 @@
|
||||||
"objectTagging",
|
"objectTagging",
|
||||||
"clipEncoding",
|
"clipEncoding",
|
||||||
"storageTemplateMigration",
|
"storageTemplateMigration",
|
||||||
|
"migration",
|
||||||
"backgroundTask",
|
"backgroundTask",
|
||||||
"search",
|
"search",
|
||||||
"recognizeFaces",
|
"recognizeFaces",
|
||||||
|
|
|
@ -57,7 +57,7 @@ export interface UploadFile {
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
private logger = new Logger(AssetService.name);
|
private logger = new Logger(AssetService.name);
|
||||||
private access: AccessCore;
|
private access: AccessCore;
|
||||||
private storageCore = new StorageCore();
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
|
@ -67,6 +67,7 @@ export class AssetService {
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.access = new AccessCore(accessRepository);
|
this.access = new AccessCore(accessRepository);
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
||||||
|
|
|
@ -307,14 +307,14 @@ describe(FacialRecognitionService.name, () => {
|
||||||
await sut.handleGenerateFaceThumbnail(face.middle);
|
await sut.handleGenerateFaceThumbnail(face.middle);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1']);
|
expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1']);
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/pe/rs');
|
||||||
expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
|
expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
|
||||||
left: 95,
|
left: 95,
|
||||||
top: 95,
|
top: 95,
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 110,
|
height: 110,
|
||||||
});
|
});
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith(croppedFace, 'upload/thumbs/user-id/person-1.jpeg', {
|
expect(mediaMock.resize).toHaveBeenCalledWith(croppedFace, 'upload/thumbs/user-id/pe/rs/person-1.jpeg', {
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
|
@ -323,7 +323,7 @@ describe(FacialRecognitionService.name, () => {
|
||||||
expect(personMock.update).toHaveBeenCalledWith({
|
expect(personMock.update).toHaveBeenCalledWith({
|
||||||
faceAssetId: 'asset-1',
|
faceAssetId: 'asset-1',
|
||||||
id: 'person-1',
|
id: 'person-1',
|
||||||
thumbnailPath: 'upload/thumbs/user-id/person-1.jpeg',
|
thumbnailPath: 'upload/thumbs/user-id/pe/rs/person-1.jpeg',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -338,7 +338,7 @@ describe(FacialRecognitionService.name, () => {
|
||||||
width: 510,
|
width: 510,
|
||||||
height: 510,
|
height: 510,
|
||||||
});
|
});
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith(croppedFace, 'upload/thumbs/user-id/person-1.jpeg', {
|
expect(mediaMock.resize).toHaveBeenCalledWith(croppedFace, 'upload/thumbs/user-id/pe/rs/person-1.jpeg', {
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
|
@ -357,7 +357,7 @@ describe(FacialRecognitionService.name, () => {
|
||||||
width: 202,
|
width: 202,
|
||||||
height: 202,
|
height: 202,
|
||||||
});
|
});
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith(croppedFace, 'upload/thumbs/user-id/person-1.jpeg', {
|
expect(mediaMock.resize).toHaveBeenCalledWith(croppedFace, 'upload/thumbs/user-id/pe/rs/person-1.jpeg', {
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Inject, Logger } from '@nestjs/common';
|
import { Inject, Logger } from '@nestjs/common';
|
||||||
import { join } from 'path';
|
|
||||||
import { IAssetRepository, WithoutProperty } from '../asset';
|
import { IAssetRepository, WithoutProperty } from '../asset';
|
||||||
import { usePagination } from '../domain.util';
|
import { usePagination } from '../domain.util';
|
||||||
import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
||||||
|
@ -13,8 +12,8 @@ import { AssetFaceId, IFaceRepository } from './face.repository';
|
||||||
|
|
||||||
export class FacialRecognitionService {
|
export class FacialRecognitionService {
|
||||||
private logger = new Logger(FacialRecognitionService.name);
|
private logger = new Logger(FacialRecognitionService.name);
|
||||||
private storageCore = new StorageCore();
|
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@ -28,6 +27,7 @@ export class FacialRecognitionService {
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.configCore = new SystemConfigCore(configRepository);
|
this.configCore = new SystemConfigCore(configRepository);
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueRecognizeFaces({ force }: IBaseJob) {
|
async handleQueueRecognizeFaces({ force }: IBaseJob) {
|
||||||
|
@ -117,6 +117,21 @@ export class FacialRecognitionService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handlePersonMigration({ id }: IEntityJob) {
|
||||||
|
const person = await this.personRepository.getById(id);
|
||||||
|
if (!person) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = this.storageCore.ensurePath(StorageFolder.THUMBNAILS, person.ownerId, `${id}.jpeg`);
|
||||||
|
if (person.thumbnailPath && person.thumbnailPath !== path) {
|
||||||
|
await this.storageRepository.moveFile(person.thumbnailPath, path);
|
||||||
|
await this.personRepository.update({ id, thumbnailPath: path });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) {
|
async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
||||||
|
@ -132,9 +147,7 @@ export class FacialRecognitionService {
|
||||||
|
|
||||||
this.logger.verbose(`Cropping face for person: ${personId}`);
|
this.logger.verbose(`Cropping face for person: ${personId}`);
|
||||||
|
|
||||||
const outputFolder = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
|
const output = this.storageCore.ensurePath(StorageFolder.THUMBNAILS, asset.ownerId, `${personId}.jpeg`);
|
||||||
const output = join(outputFolder, `${personId}.jpeg`);
|
|
||||||
this.storageRepository.mkdirSync(outputFolder);
|
|
||||||
|
|
||||||
const { x1, y1, x2, y2 } = boundingBox;
|
const { x1, y1, x2, y2 } = boundingBox;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export enum QueueName {
|
||||||
CLIP_ENCODING = 'clipEncoding',
|
CLIP_ENCODING = 'clipEncoding',
|
||||||
BACKGROUND_TASK = 'backgroundTask',
|
BACKGROUND_TASK = 'backgroundTask',
|
||||||
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
|
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
|
||||||
|
MIGRATION = 'migration',
|
||||||
SEARCH = 'search',
|
SEARCH = 'search',
|
||||||
SIDECAR = 'sidecar',
|
SIDECAR = 'sidecar',
|
||||||
LIBRARY = 'library',
|
LIBRARY = 'library',
|
||||||
|
@ -45,6 +46,11 @@ export enum JobName {
|
||||||
STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
|
STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
|
||||||
SYSTEM_CONFIG_CHANGE = 'system-config-change',
|
SYSTEM_CONFIG_CHANGE = 'system-config-change',
|
||||||
|
|
||||||
|
// migration
|
||||||
|
QUEUE_MIGRATION = 'queue-migration',
|
||||||
|
MIGRATE_ASSET = 'migrate-asset',
|
||||||
|
MIGRATE_PERSON = 'migrate-person',
|
||||||
|
|
||||||
// object tagging
|
// object tagging
|
||||||
QUEUE_OBJECT_TAGGING = 'queue-object-tagging',
|
QUEUE_OBJECT_TAGGING = 'queue-object-tagging',
|
||||||
CLASSIFY_IMAGE = 'classify-image',
|
CLASSIFY_IMAGE = 'classify-image',
|
||||||
|
@ -119,6 +125,11 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
||||||
[JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
|
[JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
|
||||||
[JobName.SYSTEM_CONFIG_CHANGE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
|
[JobName.SYSTEM_CONFIG_CHANGE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
|
||||||
|
|
||||||
|
// migration
|
||||||
|
[JobName.QUEUE_MIGRATION]: QueueName.MIGRATION,
|
||||||
|
[JobName.MIGRATE_ASSET]: QueueName.MIGRATION,
|
||||||
|
[JobName.MIGRATE_PERSON]: QueueName.MIGRATION,
|
||||||
|
|
||||||
// object tagging
|
// object tagging
|
||||||
[JobName.QUEUE_OBJECT_TAGGING]: QueueName.OBJECT_TAGGING,
|
[JobName.QUEUE_OBJECT_TAGGING]: QueueName.OBJECT_TAGGING,
|
||||||
[JobName.CLASSIFY_IMAGE]: QueueName.OBJECT_TAGGING,
|
[JobName.CLASSIFY_IMAGE]: QueueName.OBJECT_TAGGING,
|
||||||
|
|
|
@ -68,6 +68,9 @@ export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto>
|
||||||
@ApiProperty({ type: JobStatusDto })
|
@ApiProperty({ type: JobStatusDto })
|
||||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto;
|
[QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JobStatusDto })
|
||||||
|
[QueueName.MIGRATION]!: JobStatusDto;
|
||||||
|
|
||||||
@ApiProperty({ type: JobStatusDto })
|
@ApiProperty({ type: JobStatusDto })
|
||||||
[QueueName.BACKGROUND_TASK]!: JobStatusDto;
|
[QueueName.BACKGROUND_TASK]!: JobStatusDto;
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,11 @@ export type JobItem =
|
||||||
| { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE; data: IEntityJob }
|
| { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE; data: IEntityJob }
|
||||||
| { name: JobName.SYSTEM_CONFIG_CHANGE; data?: IBaseJob }
|
| { name: JobName.SYSTEM_CONFIG_CHANGE; data?: IBaseJob }
|
||||||
|
|
||||||
|
// Migration
|
||||||
|
| { name: JobName.QUEUE_MIGRATION; data?: IBaseJob }
|
||||||
|
| { name: JobName.MIGRATE_ASSET; data?: IEntityJob }
|
||||||
|
| { name: JobName.MIGRATE_PERSON; data?: IEntityJob }
|
||||||
|
|
||||||
// Metadata Extraction
|
// Metadata Extraction
|
||||||
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
||||||
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
||||||
|
|
|
@ -94,6 +94,7 @@ describe(JobService.name, () => {
|
||||||
[QueueName.OBJECT_TAGGING]: expectedJobStatus,
|
[QueueName.OBJECT_TAGGING]: expectedJobStatus,
|
||||||
[QueueName.SEARCH]: expectedJobStatus,
|
[QueueName.SEARCH]: expectedJobStatus,
|
||||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]: expectedJobStatus,
|
[QueueName.STORAGE_TEMPLATE_MIGRATION]: expectedJobStatus,
|
||||||
|
[QueueName.MIGRATION]: expectedJobStatus,
|
||||||
[QueueName.THUMBNAIL_GENERATION]: expectedJobStatus,
|
[QueueName.THUMBNAIL_GENERATION]: expectedJobStatus,
|
||||||
[QueueName.VIDEO_CONVERSION]: expectedJobStatus,
|
[QueueName.VIDEO_CONVERSION]: expectedJobStatus,
|
||||||
[QueueName.RECOGNIZE_FACES]: expectedJobStatus,
|
[QueueName.RECOGNIZE_FACES]: expectedJobStatus,
|
||||||
|
@ -229,6 +230,7 @@ describe(JobService.name, () => {
|
||||||
[QueueName.SIDECAR]: { concurrency: 10 },
|
[QueueName.SIDECAR]: { concurrency: 10 },
|
||||||
[QueueName.LIBRARY]: { concurrency: 10 },
|
[QueueName.LIBRARY]: { concurrency: 10 },
|
||||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 10 },
|
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 10 },
|
||||||
|
[QueueName.MIGRATION]: { concurrency: 10 },
|
||||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 10 },
|
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 10 },
|
||||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 10 },
|
[QueueName.VIDEO_CONVERSION]: { concurrency: 10 },
|
||||||
},
|
},
|
||||||
|
@ -242,6 +244,7 @@ describe(JobService.name, () => {
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SIDECAR, 10);
|
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.SIDECAR, 10);
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.LIBRARY, 10);
|
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.LIBRARY, 10);
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.STORAGE_TEMPLATE_MIGRATION, 10);
|
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.STORAGE_TEMPLATE_MIGRATION, 10);
|
||||||
|
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.MIGRATION, 10);
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.THUMBNAIL_GENERATION, 10);
|
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.THUMBNAIL_GENERATION, 10);
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.VIDEO_CONVERSION, 10);
|
expect(jobMock.setConcurrency).toHaveBeenCalledWith(QueueName.VIDEO_CONVERSION, 10);
|
||||||
});
|
});
|
||||||
|
|
|
@ -76,6 +76,9 @@ export class JobService {
|
||||||
case QueueName.STORAGE_TEMPLATE_MIGRATION:
|
case QueueName.STORAGE_TEMPLATE_MIGRATION:
|
||||||
return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
|
return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
|
||||||
|
|
||||||
|
case QueueName.MIGRATION:
|
||||||
|
return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION });
|
||||||
|
|
||||||
case QueueName.OBJECT_TAGGING:
|
case QueueName.OBJECT_TAGGING:
|
||||||
await this.configCore.requireFeature(FeatureFlag.TAG_IMAGE);
|
await this.configCore.requireFeature(FeatureFlag.TAG_IMAGE);
|
||||||
return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } });
|
return this.jobRepository.queue({ name: JobName.QUEUE_OBJECT_TAGGING, data: { force } });
|
||||||
|
|
|
@ -202,8 +202,8 @@ describe(MediaService.name, () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/asset-id.jpeg', {
|
expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', {
|
||||||
size: 1440,
|
size: 1440,
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
quality: 80,
|
quality: 80,
|
||||||
|
@ -211,7 +211,7 @@ describe(MediaService.name, () => {
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -220,19 +220,23 @@ describe(MediaService.name, () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id });
|
await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith('/original/path.ext', 'upload/thumbs/user-id/asset-id.jpeg', {
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
|
'/original/path.ext',
|
||||||
outputOptions: [
|
'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
'-frames:v 1',
|
{
|
||||||
'-v verbose',
|
inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
|
||||||
'-vf scale=-2:1440:flags=lanczos+accurate_rnd+bitexact+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p',
|
outputOptions: [
|
||||||
],
|
'-frames:v 1',
|
||||||
twoPass: false,
|
'-v verbose',
|
||||||
});
|
'-vf scale=-2:1440:flags=lanczos+accurate_rnd+bitexact+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p',
|
||||||
|
],
|
||||||
|
twoPass: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -241,19 +245,23 @@ describe(MediaService.name, () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id });
|
await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith('/original/path.ext', 'upload/thumbs/user-id/asset-id.jpeg', {
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
|
'/original/path.ext',
|
||||||
outputOptions: [
|
'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
'-frames:v 1',
|
{
|
||||||
'-v verbose',
|
inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
|
||||||
'-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p',
|
outputOptions: [
|
||||||
],
|
'-frames:v 1',
|
||||||
twoPass: false,
|
'-v verbose',
|
||||||
});
|
'-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p',
|
||||||
|
],
|
||||||
|
twoPass: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
resizePath: 'upload/thumbs/user-id/asset-id.jpeg',
|
resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -275,13 +283,16 @@ describe(MediaService.name, () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/asset-id.webp', {
|
expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', {
|
||||||
format: 'webp',
|
format: 'webp',
|
||||||
size: 250,
|
size: 250,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
});
|
});
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', webpPath: 'upload/thumbs/user-id/asset-id.webp' });
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
|
id: 'asset-id',
|
||||||
|
webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -375,7 +386,7 @@ describe(MediaService.name, () => {
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalled();
|
expect(storageMock.mkdirSync).toHaveBeenCalled();
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -416,7 +427,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -442,7 +453,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -471,7 +482,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -498,7 +509,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -525,7 +536,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -552,7 +563,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -603,7 +614,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -635,7 +646,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -664,7 +675,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -695,7 +706,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -728,7 +739,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -760,7 +771,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -791,7 +802,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -821,7 +832,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -851,7 +862,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -881,7 +892,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -914,7 +925,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -976,7 +987,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1014,7 +1025,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1048,7 +1059,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1083,7 +1094,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1114,7 +1125,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
inputOptions: ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1150,7 +1161,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
|
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1186,7 +1197,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
|
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1219,7 +1230,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
|
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1263,7 +1274,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
|
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1295,7 +1306,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
|
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1329,7 +1340,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
|
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1359,7 +1370,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/card1', '-filter_hw_device accel'],
|
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/card1', '-filter_hw_device accel'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1385,7 +1396,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel'],
|
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel'],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1418,7 +1429,7 @@ describe(MediaService.name, () => {
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledTimes(2);
|
expect(mediaMock.transcode).toHaveBeenCalledTimes(2);
|
||||||
expect(mediaMock.transcode).toHaveBeenLastCalledWith(
|
expect(mediaMock.transcode).toHaveBeenLastCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1455,7 +1466,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1482,7 +1493,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
@ -1509,7 +1520,7 @@ describe(MediaService.name, () => {
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { AssetEntity, AssetType, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities';
|
import { AssetEntity, AssetType, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities';
|
||||||
import { Inject, Injectable, Logger, UnsupportedMediaTypeException } from '@nestjs/common';
|
import { Inject, Injectable, Logger, UnsupportedMediaTypeException } from '@nestjs/common';
|
||||||
import { join } from 'path';
|
|
||||||
import { IAssetRepository, WithoutProperty } from '../asset';
|
import { IAssetRepository, WithoutProperty } from '../asset';
|
||||||
import { usePagination } from '../domain.util';
|
import { usePagination } from '../domain.util';
|
||||||
import { IBaseJob, IEntityJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
import { IBaseJob, IEntityJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
||||||
import { IPersonRepository } from '../person';
|
import { IPersonRepository } from '../person';
|
||||||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||||
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
||||||
|
@ -14,8 +13,8 @@ import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIC
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaService {
|
export class MediaService {
|
||||||
private logger = new Logger(MediaService.name);
|
private logger = new Logger(MediaService.name);
|
||||||
private storageCore = new StorageCore();
|
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@ -26,11 +25,10 @@ export class MediaService {
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
) {
|
) {
|
||||||
this.configCore = new SystemConfigCore(configRepository);
|
this.configCore = new SystemConfigCore(configRepository);
|
||||||
|
this.storageCore = new StorageCore(this.storageRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueGenerateThumbnails(job: IBaseJob) {
|
async handleQueueGenerateThumbnails({ force }: IBaseJob) {
|
||||||
const { force } = job;
|
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination)
|
? this.assetRepository.getAll(pagination)
|
||||||
|
@ -81,6 +79,58 @@ export class MediaService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleQueueMigration() {
|
||||||
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
|
this.assetRepository.getAll(pagination),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { active, waiting } = await this.jobRepository.getJobCounts(QueueName.MIGRATION);
|
||||||
|
if (active === 1 && waiting === 0) {
|
||||||
|
await this.storageCore.removeEmptyDirs(StorageFolder.THUMBNAILS);
|
||||||
|
await this.storageCore.removeEmptyDirs(StorageFolder.ENCODED_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const assets of assetPagination) {
|
||||||
|
for (const asset of assets) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.MIGRATE_ASSET, data: { id: asset.id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const people = await this.personRepository.getAll();
|
||||||
|
for (const person of people) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.MIGRATE_PERSON, data: { id: person.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAssetMigration({ id }: IEntityJob) {
|
||||||
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
|
if (!asset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const resizePath = this.ensureThumbnailPath(asset, 'jpeg');
|
||||||
|
const webpPath = this.ensureThumbnailPath(asset, 'webp');
|
||||||
|
const encodedVideoPath = this.ensureEncodedVideoPath(asset, 'mp4');
|
||||||
|
|
||||||
|
if (asset.resizePath && asset.resizePath !== resizePath) {
|
||||||
|
await this.storageRepository.moveFile(asset.resizePath, resizePath);
|
||||||
|
await this.assetRepository.save({ id: asset.id, resizePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.webpPath && asset.webpPath !== webpPath) {
|
||||||
|
await this.storageRepository.moveFile(asset.webpPath, webpPath);
|
||||||
|
await this.assetRepository.save({ id: asset.id, webpPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.encodedVideoPath && asset.encodedVideoPath !== encodedVideoPath) {
|
||||||
|
await this.storageRepository.moveFile(asset.encodedVideoPath, encodedVideoPath);
|
||||||
|
await this.assetRepository.save({ id: asset.id, encodedVideoPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async handleGenerateJpegThumbnail({ id }: IEntityJob) {
|
async handleGenerateJpegThumbnail({ id }: IEntityJob) {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
|
@ -184,9 +234,7 @@ export class MediaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = asset.originalPath;
|
const input = asset.originalPath;
|
||||||
const outputFolder = this.storageCore.getFolderLocation(StorageFolder.ENCODED_VIDEO, asset.ownerId);
|
const output = this.ensureEncodedVideoPath(asset, 'mp4');
|
||||||
const output = join(outputFolder, `${asset.id}.mp4`);
|
|
||||||
this.storageRepository.mkdirSync(outputFolder);
|
|
||||||
|
|
||||||
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
||||||
const mainVideoStream = this.getMainStream(videoStreams);
|
const mainVideoStream = this.getMainStream(videoStreams);
|
||||||
|
@ -330,8 +378,10 @@ export class MediaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureThumbnailPath(asset: AssetEntity, extension: string): string {
|
ensureThumbnailPath(asset: AssetEntity, extension: string): string {
|
||||||
const folderPath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
|
return this.storageCore.ensurePath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.${extension}`);
|
||||||
this.storageRepository.mkdirSync(folderPath);
|
}
|
||||||
return join(folderPath, `${asset.id}.${extension}`);
|
|
||||||
|
ensureEncodedVideoPath(asset: AssetEntity, extension: string): string {
|
||||||
|
return this.storageCore.ensurePath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.${extension}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,10 +37,10 @@ describe(PersonService.name, () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
|
||||||
storageMock = newStorageRepositoryMock();
|
|
||||||
configMock = newSystemConfigRepositoryMock();
|
configMock = newSystemConfigRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
|
personMock = newPersonRepositoryMock();
|
||||||
|
storageMock = newStorageRepositoryMock();
|
||||||
sut = new PersonService(accessMock, personMock, configMock, storageMock, jobMock);
|
sut = new PersonService(accessMock, personMock, configMock, storageMock, jobMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ import {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerInfoService {
|
export class ServerInfoService {
|
||||||
private storageCore = new StorageCore();
|
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
|
@ -25,6 +25,7 @@ export class ServerInfoService {
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.configCore = new SystemConfigCore(configRepository);
|
this.configCore = new SystemConfigCore(configRepository);
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(): Promise<ServerInfoResponseDto> {
|
async getInfo(): Promise<ServerInfoResponseDto> {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export interface MoveAssetMetadata {
|
||||||
export class StorageTemplateService {
|
export class StorageTemplateService {
|
||||||
private logger = new Logger(StorageTemplateService.name);
|
private logger = new Logger(StorageTemplateService.name);
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
private storageCore = new StorageCore();
|
private storageCore: StorageCore;
|
||||||
private storageTemplate: HandlebarsTemplateDelegate<any>;
|
private storageTemplate: HandlebarsTemplateDelegate<any>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -44,6 +44,7 @@ export class StorageTemplateService {
|
||||||
this.configCore = new SystemConfigCore(configRepository);
|
this.configCore = new SystemConfigCore(configRepository);
|
||||||
this.configCore.addValidator((config) => this.validate(config));
|
this.configCore.addValidator((config) => this.validate(config));
|
||||||
this.configCore.config$.subscribe((config) => this.onConfig(config));
|
this.configCore.config$.subscribe((config) => this.onConfig(config));
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleMigrationSingle({ id }: IEntityJob) {
|
async handleMigrationSingle({ id }: IEntityJob) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { APP_MEDIA_LOCATION } from '../domain.constant';
|
import { APP_MEDIA_LOCATION } from '../domain.constant';
|
||||||
|
import { IStorageRepository } from './storage.repository';
|
||||||
|
|
||||||
export enum StorageFolder {
|
export enum StorageFolder {
|
||||||
ENCODED_VIDEO = 'encoded-video',
|
ENCODED_VIDEO = 'encoded-video',
|
||||||
|
@ -10,6 +11,8 @@ export enum StorageFolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorageCore {
|
export class StorageCore {
|
||||||
|
constructor(private repository: IStorageRepository) {}
|
||||||
|
|
||||||
getFolderLocation(
|
getFolderLocation(
|
||||||
folder: StorageFolder.ENCODED_VIDEO | StorageFolder.UPLOAD | StorageFolder.PROFILE | StorageFolder.THUMBNAILS,
|
folder: StorageFolder.ENCODED_VIDEO | StorageFolder.UPLOAD | StorageFolder.PROFILE | StorageFolder.THUMBNAILS,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -24,4 +27,22 @@ export class StorageCore {
|
||||||
getBaseFolder(folder: StorageFolder) {
|
getBaseFolder(folder: StorageFolder) {
|
||||||
return join(APP_MEDIA_LOCATION, folder);
|
return join(APP_MEDIA_LOCATION, folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensurePath(
|
||||||
|
folder: StorageFolder.ENCODED_VIDEO | StorageFolder.UPLOAD | StorageFolder.PROFILE | StorageFolder.THUMBNAILS,
|
||||||
|
ownerId: string,
|
||||||
|
fileName: string,
|
||||||
|
): string {
|
||||||
|
const folderPath = join(
|
||||||
|
this.getFolderLocation(folder, ownerId),
|
||||||
|
fileName.substring(0, 2),
|
||||||
|
fileName.substring(2, 4),
|
||||||
|
);
|
||||||
|
this.repository.mkdirSync(folderPath);
|
||||||
|
return join(folderPath, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEmptyDirs(folder: StorageFolder) {
|
||||||
|
return this.repository.removeEmptyDirs(this.getBaseFolder(folder));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export interface IStorageRepository {
|
||||||
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
|
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
|
||||||
unlink(filepath: string): Promise<void>;
|
unlink(filepath: string): Promise<void>;
|
||||||
unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
|
unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
|
||||||
removeEmptyDirs(folder: string): Promise<void>;
|
removeEmptyDirs(folder: string, self?: boolean): Promise<void>;
|
||||||
moveFile(source: string, target: string): Promise<void>;
|
moveFile(source: string, target: string): Promise<void>;
|
||||||
checkFileExists(filepath: string, mode?: number): Promise<boolean>;
|
checkFileExists(filepath: string, mode?: number): Promise<boolean>;
|
||||||
mkdirSync(filepath: string): void;
|
mkdirSync(filepath: string): void;
|
||||||
|
|
|
@ -6,9 +6,11 @@ import { IStorageRepository } from './storage.repository';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageService {
|
export class StorageService {
|
||||||
private logger = new Logger(StorageService.name);
|
private logger = new Logger(StorageService.name);
|
||||||
private storageCore = new StorageCore();
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {}
|
constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
|
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
|
||||||
|
|
|
@ -47,6 +47,12 @@ export class SystemConfigJobDto implements Record<QueueName, JobSettingsDto> {
|
||||||
@Type(() => JobSettingsDto)
|
@Type(() => JobSettingsDto)
|
||||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobSettingsDto;
|
[QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobSettingsDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: JobSettingsDto })
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
@Type(() => JobSettingsDto)
|
||||||
|
[QueueName.MIGRATION]!: JobSettingsDto;
|
||||||
|
|
||||||
@ApiProperty({ type: JobSettingsDto })
|
@ApiProperty({ type: JobSettingsDto })
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
|
|
|
@ -53,6 +53,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||||
[QueueName.LIBRARY]: { concurrency: 1 },
|
[QueueName.LIBRARY]: { concurrency: 1 },
|
||||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
|
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
|
||||||
|
[QueueName.MIGRATION]: { concurrency: 5 },
|
||||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||||
[QueueName.LIBRARY]: { concurrency: 1 },
|
[QueueName.LIBRARY]: { concurrency: 1 },
|
||||||
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
|
[QueueName.STORAGE_TEMPLATE_MIGRATION]: { concurrency: 5 },
|
||||||
|
[QueueName.MIGRATION]: { concurrency: 5 },
|
||||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,8 +25,8 @@ import { IUserRepository } from './user.repository';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private logger = new Logger(UserService.name);
|
private logger = new Logger(UserService.name);
|
||||||
|
private storageCore: StorageCore;
|
||||||
private userCore: UserCore;
|
private userCore: UserCore;
|
||||||
private storageCore = new StorageCore();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
|
@ -37,6 +37,7 @@ export class UserService {
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
this.userCore = new UserCore(userRepository, libraryRepository, cryptoRepository);
|
this.userCore = new UserCore(userRepository, libraryRepository, cryptoRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ export enum SystemConfigKey {
|
||||||
JOB_SEARCH_CONCURRENCY = 'job.search.concurrency',
|
JOB_SEARCH_CONCURRENCY = 'job.search.concurrency',
|
||||||
JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency',
|
JOB_SIDECAR_CONCURRENCY = 'job.sidecar.concurrency',
|
||||||
JOB_LIBRARY_CONCURRENCY = 'job.library.concurrency',
|
JOB_LIBRARY_CONCURRENCY = 'job.library.concurrency',
|
||||||
|
JOB_MIGRATION_CONCURRENCY = 'job.migration.concurrency',
|
||||||
|
|
||||||
MACHINE_LEARNING_ENABLED = 'machineLearning.enabled',
|
MACHINE_LEARNING_ENABLED = 'machineLearning.enabled',
|
||||||
MACHINE_LEARNING_URL = 'machineLearning.url',
|
MACHINE_LEARNING_URL = 'machineLearning.url',
|
||||||
|
|
|
@ -66,11 +66,7 @@ export class FilesystemProvider implements IStorageRepository {
|
||||||
await fs.rm(folder, options);
|
await fs.rm(folder, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeEmptyDirs(directory: string) {
|
async removeEmptyDirs(directory: string, self: boolean = false) {
|
||||||
this._removeEmptyDirs(directory, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _removeEmptyDirs(directory: string, self: boolean) {
|
|
||||||
// lstat does not follow symlinks (in contrast to stat)
|
// lstat does not follow symlinks (in contrast to stat)
|
||||||
const stats = await fs.lstat(directory);
|
const stats = await fs.lstat(directory);
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
|
@ -78,7 +74,7 @@ export class FilesystemProvider implements IStorageRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
await Promise.all(files.map((file) => this._removeEmptyDirs(path.join(directory, file), true)));
|
await Promise.all(files.map((file) => this.removeEmptyDirs(path.join(directory, file), true)));
|
||||||
|
|
||||||
if (self) {
|
if (self) {
|
||||||
const updated = await fs.readdir(directory);
|
const updated = await fs.readdir(directory);
|
||||||
|
|
|
@ -63,6 +63,9 @@ export class AppService {
|
||||||
[JobName.SEARCH_REMOVE_FACE]: (data) => this.searchService.handleRemoveFace(data),
|
[JobName.SEARCH_REMOVE_FACE]: (data) => this.searchService.handleRemoveFace(data),
|
||||||
[JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
|
[JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
|
||||||
[JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data),
|
[JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data),
|
||||||
|
[JobName.QUEUE_MIGRATION]: () => this.mediaService.handleQueueMigration(),
|
||||||
|
[JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data),
|
||||||
|
[JobName.MIGRATE_PERSON]: (data) => this.facialRecognitionService.handlePersonMigration(data),
|
||||||
[JobName.SYSTEM_CONFIG_CHANGE]: () => this.systemConfigService.refreshConfig(),
|
[JobName.SYSTEM_CONFIG_CHANGE]: () => this.systemConfigService.refreshConfig(),
|
||||||
[JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data),
|
[JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data),
|
||||||
[JobName.GENERATE_JPEG_THUMBNAIL]: (data) => this.mediaService.handleGenerateJpegThumbnail(data),
|
[JobName.GENERATE_JPEG_THUMBNAIL]: (data) => this.mediaService.handleGenerateJpegThumbnail(data),
|
||||||
|
|
|
@ -50,7 +50,7 @@ const validate = <T>(value: T): T | null => (typeof value === 'string' ? null :
|
||||||
export class MetadataExtractionProcessor {
|
export class MetadataExtractionProcessor {
|
||||||
private logger = new Logger(MetadataExtractionProcessor.name);
|
private logger = new Logger(MetadataExtractionProcessor.name);
|
||||||
private reverseGeocodingEnabled: boolean;
|
private reverseGeocodingEnabled: boolean;
|
||||||
private storageCore = new StorageCore();
|
private storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@ -63,6 +63,7 @@ export class MetadataExtractionProcessor {
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.reverseGeocodingEnabled = !configService.get('DISABLE_REVERSE_GEOCODING');
|
this.reverseGeocodingEnabled = !configService.get('DISABLE_REVERSE_GEOCODING');
|
||||||
|
this.storageCore = new StorageCore(storageRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(deleteCache = false) {
|
async init(deleteCache = false) {
|
||||||
|
|
|
@ -131,6 +131,7 @@ export class ImmichApi {
|
||||||
[JobName.RecognizeFaces]: 'Recognize Faces',
|
[JobName.RecognizeFaces]: 'Recognize Faces',
|
||||||
[JobName.VideoConversion]: 'Transcode Videos',
|
[JobName.VideoConversion]: 'Transcode Videos',
|
||||||
[JobName.StorageTemplateMigration]: 'Storage Template Migration',
|
[JobName.StorageTemplateMigration]: 'Storage Template Migration',
|
||||||
|
[JobName.Migration]: 'Migration',
|
||||||
[JobName.BackgroundTask]: 'Background Tasks',
|
[JobName.BackgroundTask]: 'Background Tasks',
|
||||||
[JobName.Search]: 'Search',
|
[JobName.Search]: 'Search',
|
||||||
[JobName.Library]: 'Library',
|
[JobName.Library]: 'Library',
|
||||||
|
|
13
web/src/api/open-api/api.ts
generated
13
web/src/api/open-api/api.ts
generated
|
@ -307,6 +307,12 @@ export interface AllJobStatusResponseDto {
|
||||||
* @memberof AllJobStatusResponseDto
|
* @memberof AllJobStatusResponseDto
|
||||||
*/
|
*/
|
||||||
'metadataExtraction': JobStatusDto;
|
'metadataExtraction': JobStatusDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {JobStatusDto}
|
||||||
|
* @memberof AllJobStatusResponseDto
|
||||||
|
*/
|
||||||
|
'migration': JobStatusDto;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobStatusDto}
|
* @type {JobStatusDto}
|
||||||
|
@ -1779,6 +1785,7 @@ export const JobName = {
|
||||||
ClipEncoding: 'clipEncoding',
|
ClipEncoding: 'clipEncoding',
|
||||||
BackgroundTask: 'backgroundTask',
|
BackgroundTask: 'backgroundTask',
|
||||||
StorageTemplateMigration: 'storageTemplateMigration',
|
StorageTemplateMigration: 'storageTemplateMigration',
|
||||||
|
Migration: 'migration',
|
||||||
Search: 'search',
|
Search: 'search',
|
||||||
Sidecar: 'sidecar',
|
Sidecar: 'sidecar',
|
||||||
Library: 'library'
|
Library: 'library'
|
||||||
|
@ -3240,6 +3247,12 @@ export interface SystemConfigJobDto {
|
||||||
* @memberof SystemConfigJobDto
|
* @memberof SystemConfigJobDto
|
||||||
*/
|
*/
|
||||||
'metadataExtraction': JobSettingsDto;
|
'metadataExtraction': JobSettingsDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {JobSettingsDto}
|
||||||
|
* @memberof SystemConfigJobDto
|
||||||
|
*/
|
||||||
|
'migration': JobSettingsDto;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {JobSettingsDto}
|
* @type {JobSettingsDto}
|
||||||
|
|
|
@ -110,6 +110,12 @@
|
||||||
allowForceCommand: false,
|
allowForceCommand: false,
|
||||||
component: StorageMigrationDescription,
|
component: StorageMigrationDescription,
|
||||||
},
|
},
|
||||||
|
[JobName.Migration]: {
|
||||||
|
icon: FolderMove,
|
||||||
|
title: api.getJobName(JobName.Migration),
|
||||||
|
subtitle: 'Migrate thumbnails for assets and faces to the latest folder structure',
|
||||||
|
allowForceCommand: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
$: jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
|
$: jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue