mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 14:41:59 +00:00
refactor(server): system config (#9517)
This commit is contained in:
parent
7f0f016f2e
commit
984aa8fb41
46 changed files with 599 additions and 770 deletions
|
@ -96,7 +96,7 @@ SELECT * FROM "users";
|
|||
## System Config
|
||||
|
||||
```sql title="Custom settings"
|
||||
SELECT "key", "value" FROM "system_config";
|
||||
SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
|
||||
```
|
||||
|
||||
(Only used when not using the [config file](/docs/install/config-file))
|
||||
|
|
|
@ -145,7 +145,6 @@ export const utils = {
|
|||
'sessions',
|
||||
'users',
|
||||
'system_metadata',
|
||||
'system_config',
|
||||
];
|
||||
|
||||
const sql: string[] = [];
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
|
||||
export enum StorageFolder {
|
||||
ENCODED_VIDEO = 'encoded-video',
|
||||
|
@ -49,10 +49,10 @@ export class StorageCore {
|
|||
private moveRepository: IMoveRepository,
|
||||
private personRepository: IPersonRepository,
|
||||
private storageRepository: IStorageRepository,
|
||||
systemConfigRepository: ISystemConfigRepository,
|
||||
systemMetadataRepository: ISystemMetadataRepository,
|
||||
private logger: ILoggerRepository,
|
||||
) {
|
||||
this.configCore = SystemConfigCore.create(systemConfigRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
static create(
|
||||
|
@ -61,7 +61,7 @@ export class StorageCore {
|
|||
moveRepository: IMoveRepository,
|
||||
personRepository: IPersonRepository,
|
||||
storageRepository: IStorageRepository,
|
||||
systemConfigRepository: ISystemConfigRepository,
|
||||
systemMetadataRepository: ISystemMetadataRepository,
|
||||
logger: ILoggerRepository,
|
||||
) {
|
||||
if (!instance) {
|
||||
|
@ -71,7 +71,7 @@ export class StorageCore {
|
|||
moveRepository,
|
||||
personRepository,
|
||||
storageRepository,
|
||||
systemConfigRepository,
|
||||
systemMetadataRepository,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@ import * as _ from 'lodash';
|
|||
import { Subject } from 'rxjs';
|
||||
import { SystemConfig, defaults } from 'src/config';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { SystemConfigEntity, SystemConfigKey, SystemConfigValue } from 'src/entities/system-config.entity';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
|
@ -25,11 +27,11 @@ export class SystemConfigCore {
|
|||
config$ = new Subject<SystemConfig>();
|
||||
|
||||
private constructor(
|
||||
private repository: ISystemConfigRepository,
|
||||
private repository: ISystemMetadataRepository,
|
||||
private logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
static create(repository: ISystemConfigRepository, logger: ILoggerRepository) {
|
||||
static create(repository: ISystemMetadataRepository, logger: ILoggerRepository) {
|
||||
if (!instance) {
|
||||
instance = new SystemConfigCore(repository, logger);
|
||||
}
|
||||
|
@ -55,41 +57,25 @@ export class SystemConfigCore {
|
|||
}
|
||||
|
||||
async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
|
||||
const updates: SystemConfigEntity[] = [];
|
||||
const deletes: SystemConfigEntity[] = [];
|
||||
// get the difference between the new config and the default config
|
||||
const partialConfig: DeepPartial<SystemConfig> = {};
|
||||
for (const property of getKeysDeep(defaults)) {
|
||||
const newValue = _.get(newConfig, property);
|
||||
const isEmpty = newValue === undefined || newValue === null || newValue === '';
|
||||
const defaultValue = _.get(defaults, property);
|
||||
const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
|
||||
|
||||
for (const key of Object.values(SystemConfigKey)) {
|
||||
// get via dot notation
|
||||
const item = { key, value: _.get(newConfig, key) as SystemConfigValue };
|
||||
const defaultValue = _.get(defaults, key);
|
||||
const isMissing = !_.has(newConfig, key);
|
||||
|
||||
if (
|
||||
isMissing ||
|
||||
item.value === null ||
|
||||
item.value === '' ||
|
||||
item.value === defaultValue ||
|
||||
_.isEqual(item.value, defaultValue)
|
||||
) {
|
||||
deletes.push(item);
|
||||
if (isEmpty || isEqual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updates.push(item);
|
||||
_.set(partialConfig, property, newValue);
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
await this.repository.saveAll(updates);
|
||||
}
|
||||
|
||||
if (deletes.length > 0) {
|
||||
await this.repository.deleteKeys(deletes.map((item) => item.key));
|
||||
}
|
||||
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
|
||||
|
||||
const config = await this.getConfig(true);
|
||||
|
||||
this.config$.next(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
@ -103,16 +89,28 @@ export class SystemConfigCore {
|
|||
}
|
||||
|
||||
private async buildConfig() {
|
||||
const config = _.cloneDeep(defaults);
|
||||
const overrides = this.isUsingConfigFile()
|
||||
// load partial
|
||||
const partial = this.isUsingConfigFile()
|
||||
? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
|
||||
: await this.repository.load();
|
||||
: await this.repository.get(SystemMetadataKey.SYSTEM_CONFIG);
|
||||
|
||||
for (const { key, value } of overrides) {
|
||||
// set via dot notation
|
||||
_.set(config, key, value);
|
||||
// merge with defaults
|
||||
const config = _.cloneDeep(defaults);
|
||||
for (const property of getKeysDeep(partial)) {
|
||||
_.set(config, property, _.get(partial, property));
|
||||
}
|
||||
|
||||
// check for extra properties
|
||||
const unknownKeys = _.cloneDeep(config);
|
||||
for (const property of getKeysDeep(defaults)) {
|
||||
unsetDeep(unknownKeys, property);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(unknownKeys)) {
|
||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
|
||||
}
|
||||
|
||||
// validate full config
|
||||
const errors = await validate(plainToInstance(SystemConfigDto, config));
|
||||
if (errors.length > 0) {
|
||||
if (this.isUsingConfigFile()) {
|
||||
|
@ -136,36 +134,10 @@ export class SystemConfigCore {
|
|||
private async loadFromFile(filepath: string) {
|
||||
try {
|
||||
const file = await this.repository.readFile(filepath);
|
||||
const config = loadYaml(file.toString()) as any;
|
||||
const overrides: SystemConfigEntity<SystemConfigValue>[] = [];
|
||||
|
||||
for (const key of Object.values(SystemConfigKey)) {
|
||||
const value = _.get(config, key);
|
||||
this.unsetDeep(config, key);
|
||||
if (value !== undefined) {
|
||||
overrides.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(config)) {
|
||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(config, null, 2)}`);
|
||||
}
|
||||
|
||||
return overrides;
|
||||
return loadYaml(file.toString()) as unknown;
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Unable to load configuration file: ${filepath}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private unsetDeep(object: object, key: string) {
|
||||
_.unset(object, key);
|
||||
const path = key.split('.');
|
||||
while (path.pop()) {
|
||||
if (!_.isEmpty(_.get(object, path))) {
|
||||
return;
|
||||
}
|
||||
_.unset(object, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import { SessionEntity } from 'src/entities/session.entity';
|
|||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
@ -42,7 +41,6 @@ export const entities = [
|
|||
SharedLinkEntity,
|
||||
SmartInfoEntity,
|
||||
SmartSearchEntity,
|
||||
SystemConfigEntity,
|
||||
SystemMetadataEntity,
|
||||
TagEntity,
|
||||
UserEntity,
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
import { SystemConfig } from 'src/config';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
export type SystemConfigValue = string | string[] | number | boolean;
|
||||
|
||||
// https://stackoverflow.com/a/47058976
|
||||
// https://stackoverflow.com/a/70692231
|
||||
type PathsToStringProps<T> = T extends SystemConfigValue
|
||||
? []
|
||||
: {
|
||||
[K in keyof T]: [K, ...PathsToStringProps<T[K]>];
|
||||
}[keyof T];
|
||||
|
||||
type Join<T extends string[], D extends string> = T extends []
|
||||
? never
|
||||
: T extends [infer F]
|
||||
? F
|
||||
: T extends [infer F, ...infer R]
|
||||
? F extends string
|
||||
? `${F}${D}${Join<Extract<R, string[]>, D>}`
|
||||
: never
|
||||
: string;
|
||||
|
||||
// dot notation matches path in `SystemConfig`
|
||||
// TODO: migrate to key value per section
|
||||
export const SystemConfigKey = {
|
||||
FFMPEG_CRF: 'ffmpeg.crf',
|
||||
FFMPEG_THREADS: 'ffmpeg.threads',
|
||||
FFMPEG_PRESET: 'ffmpeg.preset',
|
||||
FFMPEG_TARGET_VIDEO_CODEC: 'ffmpeg.targetVideoCodec',
|
||||
FFMPEG_ACCEPTED_VIDEO_CODECS: 'ffmpeg.acceptedVideoCodecs',
|
||||
FFMPEG_TARGET_AUDIO_CODEC: 'ffmpeg.targetAudioCodec',
|
||||
FFMPEG_ACCEPTED_AUDIO_CODECS: 'ffmpeg.acceptedAudioCodecs',
|
||||
FFMPEG_TARGET_RESOLUTION: 'ffmpeg.targetResolution',
|
||||
FFMPEG_MAX_BITRATE: 'ffmpeg.maxBitrate',
|
||||
FFMPEG_BFRAMES: 'ffmpeg.bframes',
|
||||
FFMPEG_REFS: 'ffmpeg.refs',
|
||||
FFMPEG_GOP_SIZE: 'ffmpeg.gopSize',
|
||||
FFMPEG_NPL: 'ffmpeg.npl',
|
||||
FFMPEG_TEMPORAL_AQ: 'ffmpeg.temporalAQ',
|
||||
FFMPEG_CQ_MODE: 'ffmpeg.cqMode',
|
||||
FFMPEG_TWO_PASS: 'ffmpeg.twoPass',
|
||||
FFMPEG_PREFERRED_HW_DEVICE: 'ffmpeg.preferredHwDevice',
|
||||
FFMPEG_TRANSCODE: 'ffmpeg.transcode',
|
||||
FFMPEG_ACCEL: 'ffmpeg.accel',
|
||||
FFMPEG_TONEMAP: 'ffmpeg.tonemap',
|
||||
|
||||
JOB_THUMBNAIL_GENERATION_CONCURRENCY: 'job.thumbnailGeneration.concurrency',
|
||||
JOB_METADATA_EXTRACTION_CONCURRENCY: 'job.metadataExtraction.concurrency',
|
||||
JOB_VIDEO_CONVERSION_CONCURRENCY: 'job.videoConversion.concurrency',
|
||||
JOB_FACE_DETECTION_CONCURRENCY: 'job.faceDetection.concurrency',
|
||||
JOB_CLIP_ENCODING_CONCURRENCY: 'job.smartSearch.concurrency',
|
||||
JOB_BACKGROUND_TASK_CONCURRENCY: 'job.backgroundTask.concurrency',
|
||||
JOB_SEARCH_CONCURRENCY: 'job.search.concurrency',
|
||||
JOB_SIDECAR_CONCURRENCY: 'job.sidecar.concurrency',
|
||||
JOB_LIBRARY_CONCURRENCY: 'job.library.concurrency',
|
||||
JOB_MIGRATION_CONCURRENCY: 'job.migration.concurrency',
|
||||
|
||||
LIBRARY_SCAN_ENABLED: 'library.scan.enabled',
|
||||
LIBRARY_SCAN_CRON_EXPRESSION: 'library.scan.cronExpression',
|
||||
|
||||
LIBRARY_WATCH_ENABLED: 'library.watch.enabled',
|
||||
|
||||
LOGGING_ENABLED: 'logging.enabled',
|
||||
LOGGING_LEVEL: 'logging.level',
|
||||
|
||||
MACHINE_LEARNING_ENABLED: 'machineLearning.enabled',
|
||||
MACHINE_LEARNING_URL: 'machineLearning.url',
|
||||
|
||||
MACHINE_LEARNING_CLIP_ENABLED: 'machineLearning.clip.enabled',
|
||||
MACHINE_LEARNING_CLIP_MODEL_NAME: 'machineLearning.clip.modelName',
|
||||
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED: 'machineLearning.facialRecognition.enabled',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME: 'machineLearning.facialRecognition.modelName',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE: 'machineLearning.facialRecognition.minScore',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE: 'machineLearning.facialRecognition.maxDistance',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES: 'machineLearning.facialRecognition.minFaces',
|
||||
|
||||
MAP_ENABLED: 'map.enabled',
|
||||
MAP_LIGHT_STYLE: 'map.lightStyle',
|
||||
MAP_DARK_STYLE: 'map.darkStyle',
|
||||
|
||||
NOTIFICATIONS_SMTP_ENABLED: 'notifications.smtp.enabled',
|
||||
NOTIFICATIONS_SMTP_FROM: 'notifications.smtp.from',
|
||||
NOTIFICATIONS_SMTP_REPLY_TO: 'notifications.smtp.replyTo',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_IGNORE_CERT: 'notifications.smtp.transport.ignoreCert',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_HOST: 'notifications.smtp.transport.host',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_PORT: 'notifications.smtp.transport.port',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_USERNAME: 'notifications.smtp.transport.username',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_PASSWORD: 'notifications.smtp.transport.password',
|
||||
|
||||
REVERSE_GEOCODING_ENABLED: 'reverseGeocoding.enabled',
|
||||
|
||||
NEW_VERSION_CHECK_ENABLED: 'newVersionCheck.enabled',
|
||||
|
||||
OAUTH_AUTO_LAUNCH: 'oauth.autoLaunch',
|
||||
OAUTH_AUTO_REGISTER: 'oauth.autoRegister',
|
||||
OAUTH_BUTTON_TEXT: 'oauth.buttonText',
|
||||
OAUTH_CLIENT_ID: 'oauth.clientId',
|
||||
OAUTH_CLIENT_SECRET: 'oauth.clientSecret',
|
||||
OAUTH_DEFAULT_STORAGE_QUOTA: 'oauth.defaultStorageQuota',
|
||||
OAUTH_ENABLED: 'oauth.enabled',
|
||||
OAUTH_ISSUER_URL: 'oauth.issuerUrl',
|
||||
OAUTH_MOBILE_OVERRIDE_ENABLED: 'oauth.mobileOverrideEnabled',
|
||||
OAUTH_MOBILE_REDIRECT_URI: 'oauth.mobileRedirectUri',
|
||||
OAUTH_SCOPE: 'oauth.scope',
|
||||
OAUTH_SIGNING_ALGORITHM: 'oauth.signingAlgorithm',
|
||||
OAUTH_STORAGE_LABEL_CLAIM: 'oauth.storageLabelClaim',
|
||||
OAUTH_STORAGE_QUOTA_CLAIM: 'oauth.storageQuotaClaim',
|
||||
|
||||
PASSWORD_LOGIN_ENABLED: 'passwordLogin.enabled',
|
||||
|
||||
SERVER_EXTERNAL_DOMAIN: 'server.externalDomain',
|
||||
SERVER_LOGIN_PAGE_MESSAGE: 'server.loginPageMessage',
|
||||
|
||||
STORAGE_TEMPLATE_ENABLED: 'storageTemplate.enabled',
|
||||
STORAGE_TEMPLATE_HASH_VERIFICATION_ENABLED: 'storageTemplate.hashVerificationEnabled',
|
||||
STORAGE_TEMPLATE: 'storageTemplate.template',
|
||||
|
||||
IMAGE_THUMBNAIL_FORMAT: 'image.thumbnailFormat',
|
||||
IMAGE_THUMBNAIL_SIZE: 'image.thumbnailSize',
|
||||
IMAGE_PREVIEW_FORMAT: 'image.previewFormat',
|
||||
IMAGE_PREVIEW_SIZE: 'image.previewSize',
|
||||
IMAGE_QUALITY: 'image.quality',
|
||||
IMAGE_COLORSPACE: 'image.colorspace',
|
||||
IMAGE_EXTRACT_EMBEDDED: 'image.extractEmbedded',
|
||||
|
||||
TRASH_ENABLED: 'trash.enabled',
|
||||
TRASH_DAYS: 'trash.days',
|
||||
|
||||
THEME_CUSTOM_CSS: 'theme.customCss',
|
||||
|
||||
USER_DELETE_DELAY: 'user.deleteDelay',
|
||||
} as const satisfies Record<string, Join<PathsToStringProps<SystemConfig>, '.'>>;
|
||||
|
||||
export type SystemConfigKeyPaths = (typeof SystemConfigKey)[keyof typeof SystemConfigKey];
|
||||
|
||||
@Entity('system_config')
|
||||
export class SystemConfigEntity<T = SystemConfigValue> {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: SystemConfigKeyPaths;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: T;
|
||||
}
|
|
@ -1,20 +1,23 @@
|
|||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('system_metadata')
|
||||
export class SystemMetadataEntity {
|
||||
@PrimaryColumn()
|
||||
key!: string;
|
||||
export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadataKey> {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: T;
|
||||
|
||||
@Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: { [key: string]: unknown };
|
||||
value!: SystemMetadata[T];
|
||||
}
|
||||
|
||||
export enum SystemMetadataKey {
|
||||
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
||||
ADMIN_ONBOARDING = 'admin-onboarding',
|
||||
SYSTEM_CONFIG = 'system-config',
|
||||
}
|
||||
|
||||
export interface SystemMetadata extends Record<SystemMetadataKey, { [key: string]: unknown }> {
|
||||
export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, any>> {
|
||||
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
|
||||
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
|
||||
[SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
|
||||
export const ISystemConfigRepository = 'ISystemConfigRepository';
|
||||
|
||||
export interface ISystemConfigRepository {
|
||||
fetchStyle(url: string): Promise<any>;
|
||||
load(): Promise<SystemConfigEntity[]>;
|
||||
readFile(filename: string): Promise<string>;
|
||||
saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]>;
|
||||
deleteKeys(keys: string[]): Promise<void>;
|
||||
}
|
|
@ -5,4 +5,6 @@ export const ISystemMetadataRepository = 'ISystemMetadataRepository';
|
|||
export interface ISystemMetadataRepository {
|
||||
get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
|
||||
set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
|
||||
fetchStyle(url: string): Promise<any>;
|
||||
readFile(filename: string): Promise<string>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import _ from 'lodash';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveSystemConfigTable1715787369686 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const overrides = await queryRunner.query('SELECT "key", "value" FROM "system_config"');
|
||||
if (overrides.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {};
|
||||
for (const { key, value } of overrides) {
|
||||
_.set(config, key, JSON.parse(value));
|
||||
}
|
||||
|
||||
await queryRunner.query(`INSERT INTO "system_metadata" ("key", "value") VALUES ($1, $2)`, [
|
||||
'system-config',
|
||||
// yup, we're double-stringifying it
|
||||
JSON.stringify(JSON.stringify(config)),
|
||||
]);
|
||||
|
||||
await queryRunner.query(`DROP TABLE "system_config"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// no data restore, you just get the table back
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" character varying, CONSTRAINT "PK_aab69295b445016f56731f4d535" PRIMARY KEY ("key"))`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- SystemConfigRepository.load
|
||||
SELECT
|
||||
"SystemConfigEntity"."key" AS "SystemConfigEntity_key",
|
||||
"SystemConfigEntity"."value" AS "SystemConfigEntity_value"
|
||||
FROM
|
||||
"system_config" "SystemConfigEntity"
|
||||
|
||||
-- SystemConfigRepository.deleteKeys
|
||||
DELETE FROM "system_config"
|
||||
WHERE
|
||||
"key" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
@ -27,7 +27,6 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
|||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
|
@ -60,7 +59,6 @@ import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
|||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
|
@ -94,7 +92,6 @@ export const repositories = [
|
|||
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||
{ provide: ISessionRepository, useClass: SessionRepository },
|
||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||
{ provide: ITagRepository, useClass: TagRepository },
|
||||
{ provide: IMediaRepository, useClass: MediaRepository },
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class SystemConfigRepository implements ISystemConfigRepository {
|
||||
constructor(
|
||||
@InjectRepository(SystemConfigEntity)
|
||||
private repository: Repository<SystemConfigEntity>,
|
||||
) {}
|
||||
|
||||
async fetchStyle(url: string) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch data from ${url}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
load(): Promise<SystemConfigEntity[]> {
|
||||
return this.repository.find();
|
||||
}
|
||||
|
||||
readFile(filename: string): Promise<string> {
|
||||
return readFile(filename, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]> {
|
||||
return this.repository.save(items);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
@Chunked()
|
||||
async deleteKeys(keys: string[]): Promise<void> {
|
||||
await this.repository.delete({ key: In(keys) });
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
|
@ -24,4 +25,22 @@ export class SystemMetadataRepository implements ISystemMetadataRepository {
|
|||
async set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void> {
|
||||
await this.repository.upsert({ key, value }, { conflictPaths: { key: true } });
|
||||
}
|
||||
|
||||
async fetchStyle(url: string) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch data from ${url}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
readFile(filename: string): Promise<string> {
|
||||
return readFile(filename, { encoding: 'utf8' });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { assetStackStub, assetStub } from 'test/fixtures/asset.stub';
|
||||
|
@ -27,7 +27,7 @@ import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
|
@ -159,7 +159,7 @@ describe(AssetService.name, () => {
|
|||
let storageMock: Mocked<IStorageRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let assetStackMock: Mocked<IAssetStackRepository>;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
|
@ -182,7 +182,7 @@ describe(AssetService.name, () => {
|
|||
jobMock = newJobRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
partnerMock = newPartnerRepositoryMock();
|
||||
assetStackMock = newAssetStackRepositoryMock();
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
|
@ -192,7 +192,7 @@ describe(AssetService.name, () => {
|
|||
accessMock,
|
||||
assetMock,
|
||||
jobMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
storageMock,
|
||||
userMock,
|
||||
eventMock,
|
||||
|
|
|
@ -45,7 +45,7 @@ import {
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
@ -73,7 +73,7 @@ export class AssetService {
|
|||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
|
@ -84,7 +84,7 @@ export class AssetService {
|
|||
) {
|
||||
this.logger.setContext(AssetService.name);
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetFileUploadResponseDto | undefined> {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ILibraryRepository } from 'src/interfaces/library.interface';
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
|
@ -27,7 +27,7 @@ import { newLibraryRepositoryMock } from 'test/repositories/library.repository.m
|
|||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
|
||||
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mock, Mocked, vitest } from 'vitest';
|
||||
|
||||
|
@ -64,7 +64,7 @@ describe('AuthService', () => {
|
|||
let userMock: Mocked<IUserRepository>;
|
||||
let libraryMock: Mocked<ILibraryRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let sessionMock: Mocked<ISessionRepository>;
|
||||
let shareMock: Mocked<ISharedLinkRepository>;
|
||||
let keyMock: Mocked<IKeyRepository>;
|
||||
|
@ -97,7 +97,7 @@ describe('AuthService', () => {
|
|||
userMock = newUserRepositoryMock();
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
sessionMock = newSessionRepositoryMock();
|
||||
shareMock = newSharedLinkRepositoryMock();
|
||||
keyMock = newKeyRepositoryMock();
|
||||
|
@ -105,7 +105,7 @@ describe('AuthService', () => {
|
|||
sut = new AuthService(
|
||||
accessMock,
|
||||
cryptoMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
libraryMock,
|
||||
loggerMock,
|
||||
userMock,
|
||||
|
@ -121,7 +121,7 @@ describe('AuthService', () => {
|
|||
|
||||
describe('login', () => {
|
||||
it('should throw an error if password login is disabled', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.disabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.disabled);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
|
@ -199,7 +199,7 @@ describe('AuthService', () => {
|
|||
|
||||
describe('logout', () => {
|
||||
it('should return the end session endpoint', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
const auth = { user: { id: '123' } } as AuthDto;
|
||||
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
|
||||
successful: true,
|
||||
|
@ -377,7 +377,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not allow auto registering', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.noAutoRegister);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
|
@ -386,7 +386,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should link an existing user', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.noAutoRegister);
|
||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
@ -400,7 +400,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should allow auto registering by default', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
@ -415,7 +415,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should use the mobile redirect override', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.override);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.override);
|
||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
|
@ -425,7 +425,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should use the mobile redirect override for ios urls with multiple slashes', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.override);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.override);
|
||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
|
@ -435,7 +435,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should use the default quota', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
@ -448,7 +448,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should ignore an invalid storage quota', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
@ -462,7 +462,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should ignore a negative quota', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
@ -476,7 +476,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not set quota for 0 quota', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
@ -496,7 +496,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should use a valid storage quota', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
@ -518,7 +518,7 @@ describe('AuthService', () => {
|
|||
|
||||
describe('link', () => {
|
||||
it('should link an account', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' });
|
||||
|
@ -527,7 +527,7 @@ describe('AuthService', () => {
|
|||
});
|
||||
|
||||
it('should not link an already linked oauth.sub', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserEntity);
|
||||
|
||||
await expect(sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
|
||||
|
@ -540,7 +540,7 @@ describe('AuthService', () => {
|
|||
|
||||
describe('unlink', () => {
|
||||
it('should unlink an account', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.enabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.enabled);
|
||||
userMock.update.mockResolvedValue(userStub.user1);
|
||||
|
||||
await sut.unlink(authStub.user1);
|
||||
|
|
|
@ -37,7 +37,7 @@ import { ILibraryRepository } from 'src/interfaces/library.interface';
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
|
||||
|
@ -67,7 +67,7 @@ export class AuthService {
|
|||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
|
@ -77,7 +77,7 @@ export class AuthService {
|
|||
) {
|
||||
this.logger.setContext(AuthService.name);
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.configCore = SystemConfigCore.create(configRepository, logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
|
||||
|
||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
|
@ -24,7 +24,7 @@ import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
const makeMockHandlers = (status: JobStatus) => {
|
||||
|
@ -38,22 +38,22 @@ const makeMockHandlers = (status: JobStatus) => {
|
|||
describe(JobService.name, () => {
|
||||
let sut: JobService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let metricMock: Mocked<IMetricRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
metricMock = newMetricRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new JobService(assetMock, eventMock, jobMock, configMock, personMock, metricMock, loggerMock);
|
||||
sut = new JobService(assetMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -234,14 +234,14 @@ describe(JobService.name, () => {
|
|||
describe('init', () => {
|
||||
it('should register a handler for each queue', async () => {
|
||||
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
|
||||
});
|
||||
|
||||
it('should subscribe to config changes', async () => {
|
||||
await sut.init(makeMockHandlers(JobStatus.FAILED));
|
||||
|
||||
SystemConfigCore.create(newSystemConfigRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
|
||||
SystemConfigCore.create(newSystemMetadataRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
|
||||
job: {
|
||||
[QueueName.BACKGROUND_TASK]: { concurrency: 10 },
|
||||
[QueueName.SMART_SEARCH]: { concurrency: 10 },
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
|
||||
@Injectable()
|
||||
export class JobService {
|
||||
|
@ -30,13 +30,13 @@ export class JobService {
|
|||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(JobService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
}
|
||||
|
||||
async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise<JobStatusDto> {
|
||||
|
|
|
@ -5,7 +5,6 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
|
|||
import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
|
@ -14,7 +13,7 @@ import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus
|
|||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
|
@ -28,14 +27,14 @@ import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|||
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
describe(LibraryService.name, () => {
|
||||
let sut: LibraryService;
|
||||
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let libraryMock: Mocked<ILibraryRepository>;
|
||||
|
@ -44,7 +43,7 @@ describe(LibraryService.name, () => {
|
|||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
|
@ -55,7 +54,7 @@ describe(LibraryService.name, () => {
|
|||
|
||||
sut = new LibraryService(
|
||||
assetMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
cryptoMock,
|
||||
jobMock,
|
||||
libraryMock,
|
||||
|
@ -73,16 +72,13 @@ describe(LibraryService.name, () => {
|
|||
|
||||
describe('init', () => {
|
||||
it('should init cron job and subscribe to config changes', async () => {
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.LIBRARY_SCAN_ENABLED, value: true },
|
||||
{ key: SystemConfigKey.LIBRARY_SCAN_CRON_EXPRESSION, value: '0 0 * * *' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
|
||||
|
||||
await sut.init();
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(jobMock.addCronJob).toHaveBeenCalled();
|
||||
|
||||
SystemConfigCore.create(newSystemConfigRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
|
||||
SystemConfigCore.create(newSystemMetadataRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
|
@ -101,7 +97,7 @@ describe(LibraryService.name, () => {
|
|||
libraryStub.externalLibraryWithImportPaths2,
|
||||
]);
|
||||
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.get.mockImplementation((id) =>
|
||||
Promise.resolve(
|
||||
[libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find(
|
||||
|
@ -121,7 +117,7 @@ describe(LibraryService.name, () => {
|
|||
});
|
||||
|
||||
it('should not initialize watcher when watching is disabled', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||
|
||||
await sut.init();
|
||||
|
||||
|
@ -129,7 +125,7 @@ describe(LibraryService.name, () => {
|
|||
});
|
||||
|
||||
it('should not initialize watcher when lock is taken', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
databaseMock.tryLock.mockResolvedValue(false);
|
||||
|
||||
await sut.init();
|
||||
|
@ -757,7 +753,7 @@ describe(LibraryService.name, () => {
|
|||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
|
||||
const mockClose = vitest.fn();
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||
|
@ -897,7 +893,7 @@ describe(LibraryService.name, () => {
|
|||
});
|
||||
|
||||
it('should create watched with import paths', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([]);
|
||||
|
@ -1041,7 +1037,7 @@ describe(LibraryService.name, () => {
|
|||
|
||||
describe('update', () => {
|
||||
beforeEach(async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.getAll.mockResolvedValue([]);
|
||||
|
||||
await sut.init();
|
||||
|
@ -1058,7 +1054,7 @@ describe(LibraryService.name, () => {
|
|||
describe('watchAll', () => {
|
||||
describe('watching disabled', () => {
|
||||
beforeEach(async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||
|
||||
await sut.init();
|
||||
});
|
||||
|
@ -1074,7 +1070,7 @@ describe(LibraryService.name, () => {
|
|||
|
||||
describe('watching enabled', () => {
|
||||
beforeEach(async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.getAll.mockResolvedValue([]);
|
||||
await sut.init();
|
||||
});
|
||||
|
@ -1229,7 +1225,7 @@ describe(LibraryService.name, () => {
|
|||
libraryStub.externalLibraryWithImportPaths2,
|
||||
]);
|
||||
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
|
||||
libraryMock.get.mockImplementation((id) =>
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
@ -55,7 +55,7 @@ export class LibraryService {
|
|||
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILibraryRepository) private repository: ILibraryRepository,
|
||||
|
@ -64,7 +64,7 @@ export class LibraryService {
|
|||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(LibraryService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from 'src/config';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
|
@ -19,7 +18,7 @@ import { IMediaRepository } from 'src/interfaces/media.interface';
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
|
@ -33,24 +32,24 @@ import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'
|
|||
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(MediaService.name, () => {
|
||||
let sut: MediaService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let mediaMock: Mocked<IMediaRepository>;
|
||||
let moveMock: Mocked<IMoveRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
mediaMock = newMediaRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
|
@ -65,7 +64,7 @@ describe(MediaService.name, () => {
|
|||
jobMock,
|
||||
mediaMock,
|
||||
storageMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
moveMock,
|
||||
cryptoMock,
|
||||
loggerMock,
|
||||
|
@ -235,7 +234,7 @@ describe(MediaService.name, () => {
|
|||
});
|
||||
|
||||
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
|
||||
systemMock.get.mockResolvedValue({ image: { previewFormat: format } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
|
||||
|
||||
|
@ -254,7 +253,7 @@ describe(MediaService.name, () => {
|
|||
it('should delete previous preview if different path', async () => {
|
||||
const previousPreviewPath = assetStub.image.previewPath;
|
||||
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_THUMBNAIL_FORMAT, value: ImageFormat.WEBP }]);
|
||||
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await sut.handleGeneratePreview({ id: assetStub.image.id });
|
||||
|
@ -337,10 +336,9 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should always generate video thumbnail in one pass', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '5000k' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { twoPass: true, maxBitrate: '5000k' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleGeneratePreview({ id: assetStub.video.id });
|
||||
|
||||
|
@ -385,7 +383,7 @@ describe(MediaService.name, () => {
|
|||
it.each(Object.values(ImageFormat))(
|
||||
'should generate a %s thumbnail for an image when specified',
|
||||
async (format) => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_THUMBNAIL_FORMAT, value: format }]);
|
||||
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: format } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
|
||||
|
||||
|
@ -405,7 +403,7 @@ describe(MediaService.name, () => {
|
|||
it('should delete previous thumbnail if different path', async () => {
|
||||
const previousThumbnailPath = assetStub.image.thumbnailPath;
|
||||
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_THUMBNAIL_FORMAT, value: ImageFormat.WEBP }]);
|
||||
systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||
|
@ -438,7 +436,7 @@ describe(MediaService.name, () => {
|
|||
it('should extract embedded image if enabled and available', async () => {
|
||||
mediaMock.extract.mockResolvedValue(true);
|
||||
mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]);
|
||||
systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
||||
|
||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||
|
@ -463,7 +461,7 @@ describe(MediaService.name, () => {
|
|||
it('should resize original image if embedded image is too small', async () => {
|
||||
mediaMock.extract.mockResolvedValue(true);
|
||||
mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]);
|
||||
systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
||||
|
||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||
|
@ -486,7 +484,7 @@ describe(MediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should resize original image if embedded image not found', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]);
|
||||
systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
||||
|
||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||
|
@ -505,7 +503,7 @@ describe(MediaService.name, () => {
|
|||
});
|
||||
|
||||
it('should resize original image if embedded image extraction is not enabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: false }]);
|
||||
systemMock.get.mockResolvedValue({ image: { extractEmbedded: false } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
||||
|
||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||
|
@ -626,7 +624,7 @@ describe(MediaService.name, () => {
|
|||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
|
||||
expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext');
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
expect(storageMock.mkdirSync).toHaveBeenCalled();
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
|
@ -655,7 +653,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should transcode when set to all', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -671,7 +669,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should transcode when optimal and too big', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
|
@ -686,10 +684,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should transcode when policy Bitrate and bitrate higher than max bitrate', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.BITRATE },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '30M' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.BITRATE, maxBitrate: '30M' } });
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
|
@ -704,10 +699,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should not scale resolution if no target resolution', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' } });
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
|
@ -722,7 +714,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should scale horizontally when video is horizontal', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -738,7 +730,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should scale vertically when video is vertical', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -754,10 +746,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should always scale video if height is uneven', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddHeight);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -773,10 +762,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should always scale video if width is uneven', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddWidth);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -792,10 +778,9 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should copy video stream when video matches target', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { targetVideoCodec: VideoCodec.HEVC, acceptedAudioCodecs: [AudioCodec.AAC] },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -811,11 +796,13 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should not include hevc tag when target is hevc and video stream is copied from a different codec', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamH264);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEPTED_VIDEO_CODECS, value: [VideoCodec.H264, VideoCodec.HEVC] },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
targetVideoCodec: VideoCodec.HEVC,
|
||||
acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.HEVC],
|
||||
acceptedAudioCodecs: [AudioCodec.AAC],
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -831,11 +818,13 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should include hevc tag when target is hevc and copying hevc video stream', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEPTED_VIDEO_CODECS, value: [VideoCodec.H264, VideoCodec.HEVC] },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
targetVideoCodec: VideoCodec.HEVC,
|
||||
acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.HEVC],
|
||||
acceptedAudioCodecs: [AudioCodec.AAC],
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -851,7 +840,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should copy audio stream when audio matches target', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.audioStreamAac);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -867,7 +856,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should throw an exception if transcode value is invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'invalid' }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } });
|
||||
|
||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrow();
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -875,7 +864,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should not transcode if transcoding is disabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.DISABLED }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -883,7 +872,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should not transcode if target codec is invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: 'invalid' }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -892,7 +881,7 @@ describe(MediaService.name, () => {
|
|||
it('should delete existing transcode if current policy does not require transcoding', async () => {
|
||||
const asset = assetStub.hasEncodedVideo;
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.DISABLED }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
||||
assetMock.getByIds.mockResolvedValue([asset]);
|
||||
|
||||
await sut.handleVideoConversion({ id: asset.id });
|
||||
|
@ -906,7 +895,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set max bitrate if above 0', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '4500k' }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500k' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -922,10 +911,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '4500k' },
|
||||
{ key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -941,7 +927,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should fallback to one pass for h264/h265 if two-pass is enabled but no max bitrate is set', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TWO_PASS, value: true }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { twoPass: true } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -957,11 +943,13 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should transcode by bitrate in two passes for vp9 when two pass mode and max bitrate are enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '4500k' },
|
||||
{ key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
maxBitrate: '4500k',
|
||||
twoPass: true,
|
||||
targetVideoCodec: VideoCodec.VP9,
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -977,11 +965,13 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should transcode by crf in two passes for vp9 when two pass mode is enabled and max bitrate is disabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
{ key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
maxBitrate: '0',
|
||||
twoPass: true,
|
||||
targetVideoCodec: VideoCodec.VP9,
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -997,10 +987,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should configure preset for vp9', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
{ key: SystemConfigKey.FFMPEG_PRESET, value: 'slow' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, preset: 'slow' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1016,10 +1003,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should not configure preset for vp9 if invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
{ key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.VP9 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1035,10 +1019,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should configure threads if above 0', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
{ key: SystemConfigKey.FFMPEG_THREADS, value: 2 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, threads: 2 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1054,7 +1035,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should disable thread pooling for h264 if thread limit is 1', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_THREADS, value: 1 }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { threads: 1 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1070,7 +1051,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should omit thread flags for h264 if thread limit is at or below 0', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_THREADS, value: 0 }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { threads: 0 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1086,10 +1067,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should disable thread pooling for hevc if thread limit is 1', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_THREADS, value: 1 },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.HEVC } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1105,10 +1083,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should omit thread flags for hevc if thread limit is at or below 0', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_THREADS, value: 0 },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.HEVC } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1124,7 +1099,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should use av1 if specified', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1150,10 +1125,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should map `veryslow` preset to 4 for av1', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
|
||||
{ key: SystemConfigKey.FFMPEG_PRESET, value: 'veryslow' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, preset: 'veryslow' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1169,10 +1141,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set max bitrate for av1 if specified', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '2M' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, maxBitrate: '2M' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1188,10 +1157,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set threads for av1 if specified', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
|
||||
{ key: SystemConfigKey.FFMPEG_THREADS, value: 4 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1207,11 +1173,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set both bitrate and threads for av1 if specified', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
|
||||
{ key: SystemConfigKey.FFMPEG_THREADS, value: 4 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '2M' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4, maxBitrate: '2M' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1227,11 +1189,13 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should skip transcoding for audioless videos with optimal policy if video codec is correct', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: '1080p' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
targetVideoCodec: VideoCodec.HEVC,
|
||||
transcode: TranscodePolicy.OPTIMAL,
|
||||
targetResolution: '1080p',
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -1239,10 +1203,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should fail if hwaccel is enabled for an unsupported codec', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -1250,7 +1211,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should fail if hwaccel option is invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: 'invalid' }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -1258,7 +1219,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set options for nvenc', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1290,11 +1251,13 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set two pass options for nvenc when enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
{ key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
accel: TranscodeHWAccel.NVENC,
|
||||
maxBitrate: '10000k',
|
||||
twoPass: true,
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1310,10 +1273,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set vbr options for nvenc when max bitrate is enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1329,10 +1289,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set cq options for nvenc when max bitrate is disabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1348,10 +1305,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should omit preset for nvenc if invalid', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, preset: 'invalid' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1367,7 +1321,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should ignore two pass for nvenc if max bitrate is disabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1384,10 +1338,7 @@ describe(MediaService.name, () => {
|
|||
it('should set options for qsv', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1420,11 +1371,13 @@ describe(MediaService.name, () => {
|
|||
it('should set options for qsv with custom dri node', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
{ key: SystemConfigKey.FFMPEG_PREFERRED_HW_DEVICE, value: '/dev/dri/renderD128' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
accel: TranscodeHWAccel.QSV,
|
||||
maxBitrate: '10000k',
|
||||
preferredHwDevice: '/dev/dri/renderD128',
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1444,10 +1397,7 @@ describe(MediaService.name, () => {
|
|||
it('should omit preset for qsv if invalid', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
|
||||
{ key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1464,10 +1414,7 @@ describe(MediaService.name, () => {
|
|||
it('should set low power mode for qsv if target video codec is vp9', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1484,7 +1431,7 @@ describe(MediaService.name, () => {
|
|||
it('should fail for qsv if no hw devices', async () => {
|
||||
storageMock.readdir.mockResolvedValue([]);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -1493,7 +1440,7 @@ describe(MediaService.name, () => {
|
|||
it('should set options for vaapi', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1525,10 +1472,7 @@ describe(MediaService.name, () => {
|
|||
it('should set vbr options for vaapi when max bitrate is enabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1554,7 +1498,7 @@ describe(MediaService.name, () => {
|
|||
it('should set cq options for vaapi when max bitrate is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1580,10 +1524,7 @@ describe(MediaService.name, () => {
|
|||
it('should omit preset for vaapi if invalid', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI },
|
||||
{ key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1603,7 +1544,7 @@ describe(MediaService.name, () => {
|
|||
it('should prefer gpu for vaapi if available', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1623,7 +1564,7 @@ describe(MediaService.name, () => {
|
|||
it('should prefer higher index gpu node', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1643,10 +1584,9 @@ describe(MediaService.name, () => {
|
|||
it('should select specific gpu node if selected', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI },
|
||||
{ key: SystemConfigKey.FFMPEG_PREFERRED_HW_DEVICE, value: '/dev/dri/renderD128' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1666,7 +1606,7 @@ describe(MediaService.name, () => {
|
|||
it('should fallback to sw transcoding if hw transcoding fails', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
mediaMock.transcode.mockRejectedValueOnce(new Error('error'));
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
|
@ -1685,7 +1625,7 @@ describe(MediaService.name, () => {
|
|||
it('should fail for vaapi if no hw devices', async () => {
|
||||
storageMock.readdir.mockResolvedValue([]);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||
|
@ -1694,7 +1634,7 @@ describe(MediaService.name, () => {
|
|||
it('should set options for rkmpp', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1724,11 +1664,13 @@ describe(MediaService.name, () => {
|
|||
it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
accel: TranscodeHWAccel.RKMPP,
|
||||
maxBitrate: '10000k',
|
||||
targetVideoCodec: VideoCodec.HEVC,
|
||||
},
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1745,11 +1687,9 @@ describe(MediaService.name, () => {
|
|||
it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' },
|
||||
});
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1767,11 +1707,7 @@ describe(MediaService.name, () => {
|
|||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1792,7 +1728,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should tonemap when policy is required and video is hdr', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.REQUIRED }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1812,7 +1748,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should tonemap when policy is optimal and video is hdr', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
@ -1832,7 +1768,7 @@ describe(MediaService.name, () => {
|
|||
|
||||
it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TONEMAP, value: ToneMapping.MOBIUS }]);
|
||||
systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } });
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
|
|
|
@ -31,7 +31,7 @@ import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import {
|
||||
AV1Config,
|
||||
H264Config,
|
||||
|
@ -59,20 +59,20 @@ export class MediaService {
|
|||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(MediaService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
personRepository,
|
||||
storageRepository,
|
||||
configRepository,
|
||||
systemMetadataRepository,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
@ -329,7 +329,6 @@ export class MediaService {
|
|||
}
|
||||
|
||||
const { ffmpeg: config } = await this.configCore.getConfig();
|
||||
|
||||
const target = this.getTranscodeTarget(config, mainVideoStream, mainAudioStream);
|
||||
if (target === TranscodeTarget.NONE) {
|
||||
if (asset.encodedVideoPath) {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Stats } from 'node:fs';
|
|||
import { constants } from 'node:fs/promises';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
|
@ -17,7 +16,7 @@ import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interfa
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { MetadataService, Orientation } from 'src/services/metadata.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
|
@ -35,14 +34,14 @@ import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository
|
|||
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(MetadataService.name, () => {
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let cryptoRepository: Mocked<ICryptoRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let metadataMock: Mocked<IMetadataRepository>;
|
||||
|
@ -59,7 +58,6 @@ describe(MetadataService.name, () => {
|
|||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
cryptoRepository = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
metadataMock = newMetadataRepositoryMock();
|
||||
|
@ -67,6 +65,7 @@ describe(MetadataService.name, () => {
|
|||
personMock = newPersonRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
mediaMock = newMediaRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
|
@ -84,7 +83,7 @@ describe(MetadataService.name, () => {
|
|||
moveMock,
|
||||
personMock,
|
||||
storageMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
userMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
@ -108,7 +107,7 @@ describe(MetadataService.name, () => {
|
|||
});
|
||||
|
||||
it('should return if reverse geocoding is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
||||
|
||||
await sut.init();
|
||||
|
||||
|
@ -297,7 +296,7 @@ describe(MetadataService.name, () => {
|
|||
|
||||
it('should apply reverse geocoding', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
|
||||
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||
metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||
metadataMock.readTags.mockResolvedValue({
|
||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
||||
|
|
|
@ -31,7 +31,7 @@ import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interfa
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
@ -113,19 +113,19 @@ export class MetadataService {
|
|||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(MetadataService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
personRepository,
|
||||
storageRepository,
|
||||
configRepository,
|
||||
systemMetadataRepository,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ServerAsyncEvent, ServerAsyncEventMap } from 'src/interfaces/event.inte
|
|||
import { IEmailJob, IJobRepository, INotifySignupJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
|
||||
@Injectable()
|
||||
|
@ -13,14 +13,14 @@ export class NotificationService {
|
|||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(NotificationService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
}
|
||||
|
||||
init() {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Colorspace } from 'src/config';
|
|||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
|
@ -14,13 +13,14 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
|
|||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
|
@ -32,7 +32,7 @@ import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
|||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
|
@ -64,7 +64,7 @@ const detectFaceMock = {
|
|||
describe(PersonService.name, () => {
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
||||
let mediaMock: Mocked<IMediaRepository>;
|
||||
|
@ -79,7 +79,7 @@ describe(PersonService.name, () => {
|
|||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
machineLearningMock = newMachineLearningRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
|
@ -96,7 +96,7 @@ describe(PersonService.name, () => {
|
|||
moveMock,
|
||||
mediaMock,
|
||||
personMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
storageMock,
|
||||
jobMock,
|
||||
searchMock,
|
||||
|
@ -451,12 +451,12 @@ describe(PersonService.name, () => {
|
|||
|
||||
describe('handleQueueDetectFaces', () => {
|
||||
it('should skip if machine learning is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await expect(sut.handleQueueDetectFaces({})).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue missing assets', async () => {
|
||||
|
@ -528,11 +528,11 @@ describe(PersonService.name, () => {
|
|||
describe('handleQueueRecognizeFaces', () => {
|
||||
it('should skip if machine learning is disabled', async () => {
|
||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if recognition jobs are already queued', async () => {
|
||||
|
@ -609,11 +609,11 @@ describe(PersonService.name, () => {
|
|||
|
||||
describe('handleDetectFaces', () => {
|
||||
it('should skip if machine learning is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip when no resize path', async () => {
|
||||
|
@ -740,9 +740,7 @@ describe(PersonService.name, () => {
|
|||
{ face: faceStub.face1, distance: 0.4 },
|
||||
] as FaceSearchResult[];
|
||||
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||
searchMock.searchFaces.mockResolvedValue(faces);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
|
||||
|
@ -767,9 +765,7 @@ describe(PersonService.name, () => {
|
|||
{ face: faceStub.noPerson2, distance: 0.3 },
|
||||
] as FaceSearchResult[];
|
||||
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||
searchMock.searchFaces.mockResolvedValue(faces);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
personMock.create.mockResolvedValue(personStub.withName);
|
||||
|
@ -807,9 +803,7 @@ describe(PersonService.name, () => {
|
|||
{ face: faceStub.noPerson2, distance: 0.4 },
|
||||
] as FaceSearchResult[];
|
||||
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||
searchMock.searchFaces.mockResolvedValue(faces);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
personMock.create.mockResolvedValue(personStub.withName);
|
||||
|
@ -831,9 +825,7 @@ describe(PersonService.name, () => {
|
|||
{ face: faceStub.noPerson2, distance: 0.4 },
|
||||
] as FaceSearchResult[];
|
||||
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||
searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||
personMock.create.mockResolvedValue(personStub.withName);
|
||||
|
@ -849,11 +841,11 @@ describe(PersonService.name, () => {
|
|||
|
||||
describe('handleGeneratePersonThumbnail', () => {
|
||||
it('should skip if machine learning is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip a person not found', async () => {
|
||||
|
|
|
@ -45,7 +45,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
|
|||
import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { Orientation } from 'src/services/metadata.service';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
|
@ -66,7 +66,7 @@ export class PersonService {
|
|||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@Inject(IPersonRepository) private repository: IPersonRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||
|
@ -75,14 +75,14 @@ export class PersonService {
|
|||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.logger.setContext(PersonService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
cryptoRepository,
|
||||
moveRepository,
|
||||
repository,
|
||||
storageRepository,
|
||||
configRepository,
|
||||
systemMetadataRepository,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
|||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
|
@ -18,7 +18,7 @@ import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository
|
|||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
vitest.useFakeTimers();
|
||||
|
@ -26,7 +26,7 @@ vitest.useFakeTimers();
|
|||
describe(SearchService.name, () => {
|
||||
let sut: SearchService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let machineMock: Mocked<IMachineLearningRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
|
@ -36,7 +36,7 @@ describe(SearchService.name, () => {
|
|||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
machineMock = newMachineLearningRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
searchMock = newSearchRepositoryMock();
|
||||
|
@ -45,7 +45,7 @@ describe(SearchService.name, () => {
|
|||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new SearchService(
|
||||
configMock,
|
||||
systemMock,
|
||||
machineMock,
|
||||
personMock,
|
||||
searchMock,
|
||||
|
|
|
@ -23,7 +23,7 @@ import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
|||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
|
@ -31,7 +31,7 @@ export class SearchService {
|
|||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||
|
@ -41,7 +41,7 @@ export class SearchService {
|
|||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(SearchService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
}
|
||||
|
||||
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
||||
|
|
|
@ -3,14 +3,12 @@ import { IEventRepository } from 'src/interfaces/event.interface';
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { ServerInfoService } from 'src/services/server-info.service';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/system-info.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
|
@ -19,31 +17,21 @@ import { Mocked } from 'vitest';
|
|||
describe(ServerInfoService.name, () => {
|
||||
let sut: ServerInfoService;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let systemMetadataMock: Mocked<ISystemMetadataRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
serverInfoMock = newServerInfoRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
systemMetadataMock = newSystemMetadataRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new ServerInfoService(
|
||||
eventMock,
|
||||
configMock,
|
||||
userMock,
|
||||
serverInfoMock,
|
||||
storageMock,
|
||||
systemMetadataMock,
|
||||
loggerMock,
|
||||
);
|
||||
sut = new ServerInfoService(eventMock, userMock, serverInfoMock, storageMock, systemMock, loggerMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -188,7 +176,7 @@ describe(ServerInfoService.name, () => {
|
|||
trash: true,
|
||||
email: false,
|
||||
});
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -203,7 +191,7 @@ describe(ServerInfoService.name, () => {
|
|||
isOnboarded: false,
|
||||
externalDomain: '',
|
||||
});
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
||||
import { asHumanReadable } from 'src/utils/bytes';
|
||||
|
@ -34,7 +33,6 @@ export class ServerInfoService {
|
|||
|
||||
constructor(
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
|
@ -42,7 +40,7 @@ export class ServerInfoService {
|
|||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ServerInfoService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
onConnect() {}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||
import { getCLIPModelInfo } from 'src/utils/misc';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
|
||||
import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(SmartInfoService.name, () => {
|
||||
let sut: SmartInfoService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let searchMock: Mocked<ISearchRepository>;
|
||||
let machineMock: Mocked<IMachineLearningRepository>;
|
||||
|
@ -30,13 +30,13 @@ describe(SmartInfoService.name, () => {
|
|||
|
||||
beforeEach(() => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
searchMock = newSearchRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
machineMock = newMachineLearningRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock);
|
||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, systemMock, loggerMock);
|
||||
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
});
|
||||
|
@ -47,7 +47,7 @@ describe(SmartInfoService.name, () => {
|
|||
|
||||
describe('handleQueueEncodeClip', () => {
|
||||
it('should do nothing if machine learning is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
await sut.handleQueueEncodeClip({});
|
||||
|
||||
|
@ -84,7 +84,7 @@ describe(SmartInfoService.name, () => {
|
|||
|
||||
describe('handleEncodeClip', () => {
|
||||
it('should do nothing if machine learning is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
||||
|
||||
expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
|
@ -28,11 +28,11 @@ export class SmartInfoService {
|
|||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||
@Inject(ISearchRepository) private repository: ISearchRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(SmartInfoService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { SystemConfig, defaults } from 'src/config';
|
|||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetPathType } from 'src/entities/move.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
|
@ -13,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
|
@ -26,7 +25,7 @@ import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.moc
|
|||
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
|
@ -34,13 +33,13 @@ describe(StorageTemplateService.name, () => {
|
|||
let sut: StorageTemplateService;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let moveMock: Mocked<IMoveRepository>;
|
||||
let personMock: Mocked<IPersonRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let databaseMock: Mocked<IDatabaseRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -48,23 +47,23 @@ describe(StorageTemplateService.name, () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.STORAGE_TEMPLATE_ENABLED, value: true }]);
|
||||
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
|
||||
|
||||
sut = new StorageTemplateService(
|
||||
albumMock,
|
||||
assetMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
moveMock,
|
||||
personMock,
|
||||
storageMock,
|
||||
|
@ -74,7 +73,7 @@ describe(StorageTemplateService.name, () => {
|
|||
loggerMock,
|
||||
);
|
||||
|
||||
SystemConfigCore.create(configMock, loggerMock).config$.next(defaults);
|
||||
SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults);
|
||||
});
|
||||
|
||||
describe('onValidateConfig', () => {
|
||||
|
@ -108,7 +107,7 @@ describe(StorageTemplateService.name, () => {
|
|||
|
||||
describe('handleMigrationSingle', () => {
|
||||
it('should skip when storage template is disabled', async () => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.STORAGE_TEMPLATE_ENABLED, value: false }]);
|
||||
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: false } });
|
||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
||||
|
|
|
@ -28,7 +28,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { getLivePhotoMotionFilename } from 'src/utils/file';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
@ -65,7 +65,7 @@ export class StorageTemplateService {
|
|||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
|
@ -75,7 +75,7 @@ export class StorageTemplateService {
|
|||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(StorageTemplateService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
this.configCore.config$.subscribe((config) => this.onConfig(config));
|
||||
this.storageCore = StorageCore.create(
|
||||
assetRepository,
|
||||
|
@ -83,7 +83,7 @@ export class StorageTemplateService {
|
|||
moveRepository,
|
||||
personRepository,
|
||||
storageRepository,
|
||||
configRepository,
|
||||
systemMetadataRepository,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,24 +12,25 @@ import {
|
|||
VideoCodec,
|
||||
defaults,
|
||||
} from 'src/config';
|
||||
import { SystemConfigEntity, SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
const updates: SystemConfigEntity[] = [
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true },
|
||||
{ key: SystemConfigKey.TRASH_DAYS, value: 10 },
|
||||
{ key: SystemConfigKey.USER_DELETE_DELAY, value: 15 },
|
||||
];
|
||||
const partialConfig = {
|
||||
ffmpeg: { crf: 30 },
|
||||
oauth: { autoLaunch: true },
|
||||
trash: { days: 10 },
|
||||
user: { deleteDelay: 15 },
|
||||
} satisfies DeepPartial<SystemConfig>;
|
||||
|
||||
const updatedConfig = Object.freeze<SystemConfig>({
|
||||
job: {
|
||||
|
@ -171,17 +172,17 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
|
||||
describe(SystemConfigService.name, () => {
|
||||
let sut: SystemConfigService;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let smartInfoMock: Mocked<ISearchRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.IMMICH_CONFIG_FILE;
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new SystemConfigService(configMock, eventMock, loggerMock, smartInfoMock);
|
||||
sut = new SystemConfigService(systemMock, eventMock, loggerMock, smartInfoMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -190,44 +191,39 @@ describe(SystemConfigService.name, () => {
|
|||
|
||||
describe('getDefaults', () => {
|
||||
it('should return the default config', () => {
|
||||
configMock.load.mockResolvedValue(updates);
|
||||
systemMock.get.mockResolvedValue(partialConfig);
|
||||
|
||||
expect(sut.getDefaults()).toEqual(defaults);
|
||||
expect(configMock.load).not.toHaveBeenCalled();
|
||||
expect(systemMock.get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig', () => {
|
||||
it('should return the default config', async () => {
|
||||
configMock.load.mockResolvedValue([]);
|
||||
systemMock.get.mockResolvedValue({});
|
||||
|
||||
await expect(sut.getConfig()).resolves.toEqual(defaults);
|
||||
});
|
||||
|
||||
it('should merge the overrides', async () => {
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true },
|
||||
{ key: SystemConfigKey.TRASH_DAYS, value: 10 },
|
||||
{ key: SystemConfigKey.USER_DELETE_DELAY, value: 15 },
|
||||
]);
|
||||
systemMock.get.mockResolvedValue({
|
||||
ffmpeg: { crf: 30 },
|
||||
oauth: { autoLaunch: true },
|
||||
trash: { days: 10 },
|
||||
user: { deleteDelay: 15 },
|
||||
});
|
||||
|
||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
||||
});
|
||||
|
||||
it('should load the config from a json file', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
const partialConfig = {
|
||||
ffmpeg: { crf: 30 },
|
||||
oauth: { autoLaunch: true },
|
||||
trash: { days: 10 },
|
||||
user: { deleteDelay: 15 },
|
||||
};
|
||||
configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
||||
|
||||
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
});
|
||||
|
||||
it('should load the config from a yaml file', async () => {
|
||||
|
@ -242,26 +238,26 @@ describe(SystemConfigService.name, () => {
|
|||
user:
|
||||
deleteDelay: 15
|
||||
`;
|
||||
configMock.readFile.mockResolvedValue(partialConfig);
|
||||
systemMock.readFile.mockResolvedValue(partialConfig);
|
||||
|
||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
||||
|
||||
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
||||
});
|
||||
|
||||
it('should accept an empty configuration file', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
|
||||
await expect(sut.getConfig()).resolves.toEqual(defaults);
|
||||
|
||||
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
});
|
||||
|
||||
it('should allow underscores in the machine learning url', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
||||
configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
const config = await sut.getConfig();
|
||||
expect(config.machineLearning.url).toEqual('immich_machine_learning');
|
||||
|
@ -272,7 +268,7 @@ describe(SystemConfigService.name, () => {
|
|||
const partialConfig = `
|
||||
unknownOption: true
|
||||
`;
|
||||
configMock.readFile.mockResolvedValue(partialConfig);
|
||||
systemMock.readFile.mockResolvedValue(partialConfig);
|
||||
|
||||
await sut.getConfig();
|
||||
expect(loggerMock.warn).toHaveBeenCalled();
|
||||
|
@ -290,7 +286,7 @@ describe(SystemConfigService.name, () => {
|
|||
for (const test of tests) {
|
||||
it(`should ${test.should}`, async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||
|
||||
if (test.warn) {
|
||||
await sut.getConfig();
|
||||
|
@ -338,20 +334,20 @@ describe(SystemConfigService.name, () => {
|
|||
|
||||
describe('updateConfig', () => {
|
||||
it('should update the config and emit client and server events', async () => {
|
||||
configMock.load.mockResolvedValue(updates);
|
||||
systemMock.get.mockResolvedValue(partialConfig);
|
||||
|
||||
await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig);
|
||||
|
||||
expect(eventMock.clientBroadcast).toHaveBeenCalled();
|
||||
expect(eventMock.serverSend).toHaveBeenCalledWith(ServerEvent.CONFIG_UPDATE, null);
|
||||
expect(configMock.saveAll).toHaveBeenCalledWith(updates);
|
||||
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
|
||||
});
|
||||
|
||||
it('should throw an error if a config file is in use', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
configMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||
await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(configMock.saveAll).not.toHaveBeenCalled();
|
||||
expect(systemMock.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -24,14 +24,14 @@ import {
|
|||
} from 'src/interfaces/event.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
|
||||
@Injectable()
|
||||
export class SystemConfigService {
|
||||
private core: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(ISystemConfigRepository) private repository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
|||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
|
@ -25,7 +25,7 @@ import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|||
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
|
@ -44,12 +44,12 @@ describe(UserService.name, () => {
|
|||
let jobMock: Mocked<IJobRepository>;
|
||||
let libraryMock: Mocked<ILibraryRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
cryptoRepositoryMock = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
|
@ -63,7 +63,7 @@ describe(UserService.name, () => {
|
|||
jobMock,
|
||||
libraryMock,
|
||||
storageMock,
|
||||
configMock,
|
||||
systemMock,
|
||||
userMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
@ -486,7 +486,7 @@ describe(UserService.name, () => {
|
|||
});
|
||||
|
||||
it('should skip users not ready for deletion - deleteDelay30', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.deleteDelay30);
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.deleteDelay30);
|
||||
userMock.getDeletedUsers.mockResolvedValue([
|
||||
{},
|
||||
{ deletedAt: undefined },
|
||||
|
|
|
@ -13,7 +13,7 @@ import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/j
|
|||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||
|
||||
|
@ -28,13 +28,13 @@ export class UserService {
|
|||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
|
||||
this.logger.setContext(UserService.name);
|
||||
this.configCore = SystemConfigCore.create(configRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
async listUsers(): Promise<UserResponseDto[]> {
|
||||
|
|
52
server/src/utils/misc.spec.ts
Normal file
52
server/src/utils/misc.spec.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('getKeysDeep', () => {
|
||||
it('should handle an empty object', () => {
|
||||
expect(getKeysDeep({})).toEqual([]);
|
||||
});
|
||||
|
||||
it('should list properties', () => {
|
||||
expect(
|
||||
getKeysDeep({
|
||||
foo: 'bar',
|
||||
flag: true,
|
||||
count: 42,
|
||||
}),
|
||||
).toEqual(['foo', 'flag', 'count']);
|
||||
});
|
||||
|
||||
it('should skip undefined properties', () => {
|
||||
expect(getKeysDeep({ foo: 'bar', hello: undefined })).toEqual(['foo']);
|
||||
});
|
||||
|
||||
it('should skip array indices', () => {
|
||||
expect(getKeysDeep({ foo: 'bar', hello: ['foo', 'bar'] })).toEqual(['foo', 'hello']);
|
||||
expect(getKeysDeep({ foo: 'bar', nested: { hello: ['foo', 'bar'] } })).toEqual(['foo', 'nested.hello']);
|
||||
});
|
||||
|
||||
it('should list nested properties', () => {
|
||||
expect(getKeysDeep({ foo: 'bar', hello: { world: true } })).toEqual(['foo', 'hello.world']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsetDeep', () => {
|
||||
it('should remove a property', () => {
|
||||
expect(unsetDeep({ hello: 'world', foo: 'bar' }, 'foo')).toEqual({ hello: 'world' });
|
||||
});
|
||||
|
||||
it('should remove the last property', () => {
|
||||
expect(unsetDeep({ foo: 'bar' }, 'foo')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove a nested property', () => {
|
||||
expect(unsetDeep({ foo: 'bar', nested: { enabled: true, count: 42 } }, 'nested.enabled')).toEqual({
|
||||
foo: 'bar',
|
||||
nested: { count: 42 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should clean up an empty property', () => {
|
||||
expect(unsetDeep({ foo: 'bar', nested: { enabled: true } }, 'nested.enabled')).toEqual({ foo: 'bar' });
|
||||
});
|
||||
});
|
|
@ -16,6 +16,47 @@ import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
|
|||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Metadata } from 'src/middleware/auth.guard';
|
||||
|
||||
/**
|
||||
* @returns a list of strings representing the keys of the object in dot notation
|
||||
*/
|
||||
export const getKeysDeep = (target: unknown, path: string[] = []) => {
|
||||
if (!target || typeof target !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const obj = target as object;
|
||||
|
||||
const properties: string[] = [];
|
||||
for (const key of Object.keys(obj as object)) {
|
||||
const value = obj[key as keyof object];
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_.isObject(value) && !_.isArray(value)) {
|
||||
properties.push(...getKeysDeep(value, [...path, key]));
|
||||
continue;
|
||||
}
|
||||
|
||||
properties.push([...path, key].join('.'));
|
||||
}
|
||||
|
||||
return properties;
|
||||
};
|
||||
|
||||
export const unsetDeep = (object: unknown, key: string) => {
|
||||
const parts = key.split('.');
|
||||
while (parts.length > 0) {
|
||||
_.unset(object, parts);
|
||||
parts.pop();
|
||||
if (!_.isEmpty(_.get(object, parts))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _.isEmpty(object) ? undefined : object;
|
||||
};
|
||||
|
||||
const isMachineLearningEnabled = (machineLearning: SystemConfig['machineLearning']) => machineLearning.enabled;
|
||||
export const isSmartSearchEnabled = (machineLearning: SystemConfig['machineLearning']) =>
|
||||
isMachineLearningEnabled(machineLearning) && machineLearning.clip.enabled;
|
||||
|
|
105
server/test/fixtures/system-config.stub.ts
vendored
105
server/test/fixtures/system-config.stub.ts
vendored
|
@ -1,33 +1,74 @@
|
|||
import { SystemConfigEntity, SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
export const systemConfigStub: Record<string, SystemConfigEntity[]> = {
|
||||
defaults: [],
|
||||
enabled: [
|
||||
{ key: SystemConfigKey.OAUTH_ENABLED, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: false },
|
||||
{ key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
|
||||
],
|
||||
disabled: [{ key: SystemConfigKey.PASSWORD_LOGIN_ENABLED, value: false }],
|
||||
noAutoRegister: [
|
||||
{ key: SystemConfigKey.OAUTH_ENABLED, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: false },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: false },
|
||||
{ key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
|
||||
],
|
||||
override: [
|
||||
{ key: SystemConfigKey.OAUTH_ENABLED, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_MOBILE_OVERRIDE_ENABLED, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_MOBILE_REDIRECT_URI, value: 'http://mobile-redirect' },
|
||||
{ key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
|
||||
],
|
||||
withDefaultStorageQuota: [
|
||||
{ key: SystemConfigKey.OAUTH_ENABLED, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
|
||||
{ key: SystemConfigKey.OAUTH_DEFAULT_STORAGE_QUOTA, value: 1 },
|
||||
],
|
||||
deleteDelay30: [{ key: SystemConfigKey.USER_DELETE_DELAY, value: 30 }],
|
||||
libraryWatchEnabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: true }],
|
||||
libraryWatchDisabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: false }],
|
||||
};
|
||||
export const systemConfigStub = {
|
||||
enabled: {
|
||||
oauth: {
|
||||
enabled: true,
|
||||
autoRegister: true,
|
||||
autoLaunch: false,
|
||||
buttonText: 'OAuth',
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
passwordLogin: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
noAutoRegister: {
|
||||
oauth: {
|
||||
enabled: true,
|
||||
autoRegister: false,
|
||||
autoLaunch: false,
|
||||
buttonText: 'OAuth',
|
||||
},
|
||||
},
|
||||
override: {
|
||||
oauth: {
|
||||
enabled: true,
|
||||
autoRegister: true,
|
||||
mobileOverrideEnabled: true,
|
||||
mobileRedirectUri: 'http://mobile-redirect',
|
||||
buttonText: 'OAuth',
|
||||
},
|
||||
},
|
||||
withDefaultStorageQuota: {
|
||||
oauth: {
|
||||
enabled: true,
|
||||
autoRegister: true,
|
||||
defaultStorageQuota: 1,
|
||||
},
|
||||
},
|
||||
deleteDelay30: {
|
||||
user: {
|
||||
deleteDelay: 30,
|
||||
},
|
||||
},
|
||||
libraryWatchEnabled: {
|
||||
library: {
|
||||
watch: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
libraryWatchDisabled: {
|
||||
library: {
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
libraryScan: {
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
cronExpression: '0 0 * * *',
|
||||
},
|
||||
},
|
||||
},
|
||||
machineLearningDisabled: {
|
||||
machineLearning: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, DeepPartial<SystemConfig>>;
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newSystemConfigRepositoryMock = (reset = true): Mocked<ISystemConfigRepository> => {
|
||||
if (reset) {
|
||||
SystemConfigCore.reset();
|
||||
}
|
||||
|
||||
return {
|
||||
fetchStyle: vitest.fn(),
|
||||
load: vitest.fn().mockResolvedValue([]),
|
||||
readFile: vitest.fn(),
|
||||
saveAll: vitest.fn().mockResolvedValue([]),
|
||||
deleteKeys: vitest.fn(),
|
||||
};
|
||||
};
|
|
@ -1,9 +1,16 @@
|
|||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newSystemMetadataRepositoryMock = (): Mocked<ISystemMetadataRepository> => {
|
||||
export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMetadataRepository> => {
|
||||
if (reset) {
|
||||
SystemConfigCore.reset();
|
||||
}
|
||||
|
||||
return {
|
||||
get: vitest.fn() as any,
|
||||
set: vitest.fn(),
|
||||
readFile: vitest.fn(),
|
||||
fetchStyle: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue