mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	refactor(server): system config (#9517)
This commit is contained in:
		
							parent
							
								
									7f0f016f2e
								
							
						
					
					
						commit
						984aa8fb41
					
				@ -96,7 +96,7 @@ SELECT * FROM "users";
 | 
				
			|||||||
## System Config
 | 
					## System Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sql title="Custom settings"
 | 
					```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))
 | 
					(Only used when not using the [config file](/docs/install/config-file))
 | 
				
			||||||
 | 
				
			|||||||
@ -145,7 +145,6 @@ export const utils = {
 | 
				
			|||||||
        'sessions',
 | 
					        'sessions',
 | 
				
			||||||
        'users',
 | 
					        'users',
 | 
				
			||||||
        'system_metadata',
 | 
					        'system_metadata',
 | 
				
			||||||
        'system_config',
 | 
					 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const sql: string[] = [];
 | 
					      const sql: string[] = [];
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			|||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
 | 
					import { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 {
 | 
					export enum StorageFolder {
 | 
				
			||||||
  ENCODED_VIDEO = 'encoded-video',
 | 
					  ENCODED_VIDEO = 'encoded-video',
 | 
				
			||||||
@ -49,10 +49,10 @@ export class StorageCore {
 | 
				
			|||||||
    private moveRepository: IMoveRepository,
 | 
					    private moveRepository: IMoveRepository,
 | 
				
			||||||
    private personRepository: IPersonRepository,
 | 
					    private personRepository: IPersonRepository,
 | 
				
			||||||
    private storageRepository: IStorageRepository,
 | 
					    private storageRepository: IStorageRepository,
 | 
				
			||||||
    systemConfigRepository: ISystemConfigRepository,
 | 
					    systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    private logger: ILoggerRepository,
 | 
					    private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(systemConfigRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static create(
 | 
					  static create(
 | 
				
			||||||
@ -61,7 +61,7 @@ export class StorageCore {
 | 
				
			|||||||
    moveRepository: IMoveRepository,
 | 
					    moveRepository: IMoveRepository,
 | 
				
			||||||
    personRepository: IPersonRepository,
 | 
					    personRepository: IPersonRepository,
 | 
				
			||||||
    storageRepository: IStorageRepository,
 | 
					    storageRepository: IStorageRepository,
 | 
				
			||||||
    systemConfigRepository: ISystemConfigRepository,
 | 
					    systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    logger: ILoggerRepository,
 | 
					    logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    if (!instance) {
 | 
					    if (!instance) {
 | 
				
			||||||
@ -71,7 +71,7 @@ export class StorageCore {
 | 
				
			|||||||
        moveRepository,
 | 
					        moveRepository,
 | 
				
			||||||
        personRepository,
 | 
					        personRepository,
 | 
				
			||||||
        storageRepository,
 | 
					        storageRepository,
 | 
				
			||||||
        systemConfigRepository,
 | 
					        systemMetadataRepository,
 | 
				
			||||||
        logger,
 | 
					        logger,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -7,10 +7,12 @@ import * as _ from 'lodash';
 | 
				
			|||||||
import { Subject } from 'rxjs';
 | 
					import { Subject } from 'rxjs';
 | 
				
			||||||
import { SystemConfig, defaults } from 'src/config';
 | 
					import { SystemConfig, defaults } from 'src/config';
 | 
				
			||||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
 | 
					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 { DatabaseLock } from 'src/interfaces/database.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.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>;
 | 
					export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,11 +27,11 @@ export class SystemConfigCore {
 | 
				
			|||||||
  config$ = new Subject<SystemConfig>();
 | 
					  config$ = new Subject<SystemConfig>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private constructor(
 | 
					  private constructor(
 | 
				
			||||||
    private repository: ISystemConfigRepository,
 | 
					    private repository: ISystemMetadataRepository,
 | 
				
			||||||
    private logger: ILoggerRepository,
 | 
					    private logger: ILoggerRepository,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static create(repository: ISystemConfigRepository, logger: ILoggerRepository) {
 | 
					  static create(repository: ISystemMetadataRepository, logger: ILoggerRepository) {
 | 
				
			||||||
    if (!instance) {
 | 
					    if (!instance) {
 | 
				
			||||||
      instance = new SystemConfigCore(repository, logger);
 | 
					      instance = new SystemConfigCore(repository, logger);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -55,41 +57,25 @@ export class SystemConfigCore {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
 | 
					  async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
 | 
				
			||||||
    const updates: SystemConfigEntity[] = [];
 | 
					    // get the difference between the new config and the default config
 | 
				
			||||||
    const deletes: SystemConfigEntity[] = [];
 | 
					    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)) {
 | 
					      if (isEmpty || isEqual) {
 | 
				
			||||||
      // 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);
 | 
					 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      updates.push(item);
 | 
					      _.set(partialConfig, property, newValue);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (updates.length > 0) {
 | 
					    await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
 | 
				
			||||||
      await this.repository.saveAll(updates);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (deletes.length > 0) {
 | 
					 | 
				
			||||||
      await this.repository.deleteKeys(deletes.map((item) => item.key));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const config = await this.getConfig(true);
 | 
					    const config = await this.getConfig(true);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.config$.next(config);
 | 
					    this.config$.next(config);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return config;
 | 
					    return config;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -103,16 +89,28 @@ export class SystemConfigCore {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async buildConfig() {
 | 
					  private async buildConfig() {
 | 
				
			||||||
    const config = _.cloneDeep(defaults);
 | 
					    // load partial
 | 
				
			||||||
    const overrides = this.isUsingConfigFile()
 | 
					    const partial = this.isUsingConfigFile()
 | 
				
			||||||
      ? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
 | 
					      ? 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) {
 | 
					    // merge with defaults
 | 
				
			||||||
      // set via dot notation
 | 
					    const config = _.cloneDeep(defaults);
 | 
				
			||||||
      _.set(config, key, value);
 | 
					    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));
 | 
					    const errors = await validate(plainToInstance(SystemConfigDto, config));
 | 
				
			||||||
    if (errors.length > 0) {
 | 
					    if (errors.length > 0) {
 | 
				
			||||||
      if (this.isUsingConfigFile()) {
 | 
					      if (this.isUsingConfigFile()) {
 | 
				
			||||||
@ -136,36 +134,10 @@ export class SystemConfigCore {
 | 
				
			|||||||
  private async loadFromFile(filepath: string) {
 | 
					  private async loadFromFile(filepath: string) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const file = await this.repository.readFile(filepath);
 | 
					      const file = await this.repository.readFile(filepath);
 | 
				
			||||||
      const config = loadYaml(file.toString()) as any;
 | 
					      return loadYaml(file.toString()) as unknown;
 | 
				
			||||||
      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;
 | 
					 | 
				
			||||||
    } catch (error: Error | any) {
 | 
					    } catch (error: Error | any) {
 | 
				
			||||||
      this.logger.error(`Unable to load configuration file: ${filepath}`);
 | 
					      this.logger.error(`Unable to load configuration file: ${filepath}`);
 | 
				
			||||||
      throw error;
 | 
					      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 { SharedLinkEntity } from 'src/entities/shared-link.entity';
 | 
				
			||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 | 
					import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 | 
				
			||||||
import { SmartSearchEntity } from 'src/entities/smart-search.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 { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
 | 
				
			||||||
import { TagEntity } from 'src/entities/tag.entity';
 | 
					import { TagEntity } from 'src/entities/tag.entity';
 | 
				
			||||||
import { UserEntity } from 'src/entities/user.entity';
 | 
					import { UserEntity } from 'src/entities/user.entity';
 | 
				
			||||||
@ -42,7 +41,6 @@ export const entities = [
 | 
				
			|||||||
  SharedLinkEntity,
 | 
					  SharedLinkEntity,
 | 
				
			||||||
  SmartInfoEntity,
 | 
					  SmartInfoEntity,
 | 
				
			||||||
  SmartSearchEntity,
 | 
					  SmartSearchEntity,
 | 
				
			||||||
  SystemConfigEntity,
 | 
					 | 
				
			||||||
  SystemMetadataEntity,
 | 
					  SystemMetadataEntity,
 | 
				
			||||||
  TagEntity,
 | 
					  TagEntity,
 | 
				
			||||||
  UserEntity,
 | 
					  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')
 | 
					@Entity('system_metadata')
 | 
				
			||||||
export class SystemMetadataEntity {
 | 
					export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadataKey> {
 | 
				
			||||||
  @PrimaryColumn()
 | 
					  @PrimaryColumn({ type: 'varchar' })
 | 
				
			||||||
  key!: string;
 | 
					  key!: T;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } })
 | 
					  @Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } })
 | 
				
			||||||
  value!: { [key: string]: unknown };
 | 
					  value!: SystemMetadata[T];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum SystemMetadataKey {
 | 
					export enum SystemMetadataKey {
 | 
				
			||||||
  REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
 | 
					  REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
 | 
				
			||||||
  ADMIN_ONBOARDING = 'admin-onboarding',
 | 
					  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.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
 | 
				
			||||||
  [SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
 | 
					  [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 {
 | 
					export interface ISystemMetadataRepository {
 | 
				
			||||||
  get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
 | 
					  get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
 | 
				
			||||||
  set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
 | 
					  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 { ISessionRepository } from 'src/interfaces/session.interface';
 | 
				
			||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
 | 
					import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
				
			||||||
import { ITagRepository } from 'src/interfaces/tag.interface';
 | 
					import { ITagRepository } from 'src/interfaces/tag.interface';
 | 
				
			||||||
import { IUserRepository } from 'src/interfaces/user.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 { SessionRepository } from 'src/repositories/session.repository';
 | 
				
			||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
 | 
					import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
 | 
				
			||||||
import { StorageRepository } from 'src/repositories/storage.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 { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
 | 
				
			||||||
import { TagRepository } from 'src/repositories/tag.repository';
 | 
					import { TagRepository } from 'src/repositories/tag.repository';
 | 
				
			||||||
import { UserRepository } from 'src/repositories/user.repository';
 | 
					import { UserRepository } from 'src/repositories/user.repository';
 | 
				
			||||||
@ -94,7 +92,6 @@ export const repositories = [
 | 
				
			|||||||
  { provide: ISearchRepository, useClass: SearchRepository },
 | 
					  { provide: ISearchRepository, useClass: SearchRepository },
 | 
				
			||||||
  { provide: ISessionRepository, useClass: SessionRepository },
 | 
					  { provide: ISessionRepository, useClass: SessionRepository },
 | 
				
			||||||
  { provide: IStorageRepository, useClass: StorageRepository },
 | 
					  { provide: IStorageRepository, useClass: StorageRepository },
 | 
				
			||||||
  { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
 | 
					 | 
				
			||||||
  { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
 | 
					  { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
 | 
				
			||||||
  { provide: ITagRepository, useClass: TagRepository },
 | 
					  { provide: ITagRepository, useClass: TagRepository },
 | 
				
			||||||
  { provide: IMediaRepository, useClass: MediaRepository },
 | 
					  { 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 { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { readFile } from 'node:fs/promises';
 | 
				
			||||||
import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity';
 | 
					import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity';
 | 
				
			||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
					import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
				
			||||||
import { Instrumentation } from 'src/utils/instrumentation';
 | 
					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> {
 | 
					  async set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void> {
 | 
				
			||||||
    await this.repository.upsert({ key, value }, { conflictPaths: { key: true } });
 | 
					    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 { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
					import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { AssetService } from 'src/services/asset.service';
 | 
					import { AssetService } from 'src/services/asset.service';
 | 
				
			||||||
import { assetStackStub, assetStub } from 'test/fixtures/asset.stub';
 | 
					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 { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
 | 
					import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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 { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
import { Mocked, vitest } from 'vitest';
 | 
					import { Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -159,7 +159,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
  let storageMock: Mocked<IStorageRepository>;
 | 
					  let storageMock: Mocked<IStorageRepository>;
 | 
				
			||||||
  let userMock: Mocked<IUserRepository>;
 | 
					  let userMock: Mocked<IUserRepository>;
 | 
				
			||||||
  let eventMock: Mocked<IEventRepository>;
 | 
					  let eventMock: Mocked<IEventRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let partnerMock: Mocked<IPartnerRepository>;
 | 
					  let partnerMock: Mocked<IPartnerRepository>;
 | 
				
			||||||
  let assetStackMock: Mocked<IAssetStackRepository>;
 | 
					  let assetStackMock: Mocked<IAssetStackRepository>;
 | 
				
			||||||
  let albumMock: Mocked<IAlbumRepository>;
 | 
					  let albumMock: Mocked<IAlbumRepository>;
 | 
				
			||||||
@ -182,7 +182,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    partnerMock = newPartnerRepositoryMock();
 | 
					    partnerMock = newPartnerRepositoryMock();
 | 
				
			||||||
    assetStackMock = newAssetStackRepositoryMock();
 | 
					    assetStackMock = newAssetStackRepositoryMock();
 | 
				
			||||||
    albumMock = newAlbumRepositoryMock();
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
@ -192,7 +192,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
      accessMock,
 | 
					      accessMock,
 | 
				
			||||||
      assetMock,
 | 
					      assetMock,
 | 
				
			||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
      userMock,
 | 
					      userMock,
 | 
				
			||||||
      eventMock,
 | 
					      eventMock,
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,7 @@ import {
 | 
				
			|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
					import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { mimeTypes } from 'src/utils/mime-types';
 | 
					import { mimeTypes } from 'src/utils/mime-types';
 | 
				
			||||||
import { usePagination } from 'src/utils/pagination';
 | 
					import { usePagination } from 'src/utils/pagination';
 | 
				
			||||||
@ -73,7 +73,7 @@ export class AssetService {
 | 
				
			|||||||
    @Inject(IAccessRepository) accessRepository: IAccessRepository,
 | 
					    @Inject(IAccessRepository) accessRepository: IAccessRepository,
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
					    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
				
			||||||
@ -84,7 +84,7 @@ export class AssetService {
 | 
				
			|||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(AssetService.name);
 | 
					    this.logger.setContext(AssetService.name);
 | 
				
			||||||
    this.access = AccessCore.create(accessRepository);
 | 
					    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> {
 | 
					  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 { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
 | 
					import { ISessionRepository } from 'src/interfaces/session.interface';
 | 
				
			||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { AuthService } from 'src/services/auth.service';
 | 
					import { AuthService } from 'src/services/auth.service';
 | 
				
			||||||
import { keyStub } from 'test/fixtures/api-key.stub';
 | 
					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 { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
 | 
					import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
 | 
				
			||||||
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.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 { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
import { Mock, Mocked, vitest } from 'vitest';
 | 
					import { Mock, Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -64,7 +64,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
  let userMock: Mocked<IUserRepository>;
 | 
					  let userMock: Mocked<IUserRepository>;
 | 
				
			||||||
  let libraryMock: Mocked<ILibraryRepository>;
 | 
					  let libraryMock: Mocked<ILibraryRepository>;
 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let sessionMock: Mocked<ISessionRepository>;
 | 
					  let sessionMock: Mocked<ISessionRepository>;
 | 
				
			||||||
  let shareMock: Mocked<ISharedLinkRepository>;
 | 
					  let shareMock: Mocked<ISharedLinkRepository>;
 | 
				
			||||||
  let keyMock: Mocked<IKeyRepository>;
 | 
					  let keyMock: Mocked<IKeyRepository>;
 | 
				
			||||||
@ -97,7 +97,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
    libraryMock = newLibraryRepositoryMock();
 | 
					    libraryMock = newLibraryRepositoryMock();
 | 
				
			||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    loggerMock = newLoggerRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    sessionMock = newSessionRepositoryMock();
 | 
					    sessionMock = newSessionRepositoryMock();
 | 
				
			||||||
    shareMock = newSharedLinkRepositoryMock();
 | 
					    shareMock = newSharedLinkRepositoryMock();
 | 
				
			||||||
    keyMock = newKeyRepositoryMock();
 | 
					    keyMock = newKeyRepositoryMock();
 | 
				
			||||||
@ -105,7 +105,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    sut = new AuthService(
 | 
					    sut = new AuthService(
 | 
				
			||||||
      accessMock,
 | 
					      accessMock,
 | 
				
			||||||
      cryptoMock,
 | 
					      cryptoMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      libraryMock,
 | 
					      libraryMock,
 | 
				
			||||||
      loggerMock,
 | 
					      loggerMock,
 | 
				
			||||||
      userMock,
 | 
					      userMock,
 | 
				
			||||||
@ -121,7 +121,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('login', () => {
 | 
					  describe('login', () => {
 | 
				
			||||||
    it('should throw an error if password login is disabled', async () => {
 | 
					    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);
 | 
					      await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -199,7 +199,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('logout', () => {
 | 
					  describe('logout', () => {
 | 
				
			||||||
    it('should return the end session endpoint', async () => {
 | 
					    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;
 | 
					      const auth = { user: { id: '123' } } as AuthDto;
 | 
				
			||||||
      await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
 | 
					      await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
 | 
				
			||||||
        successful: true,
 | 
					        successful: true,
 | 
				
			||||||
@ -377,7 +377,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not allow auto registering', async () => {
 | 
					    it('should not allow auto registering', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.noAutoRegister);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
 | 
					      await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
 | 
				
			||||||
        BadRequestException,
 | 
					        BadRequestException,
 | 
				
			||||||
@ -386,7 +386,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should link an existing user', async () => {
 | 
					    it('should link an existing user', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.noAutoRegister);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(userStub.user1);
 | 
					      userMock.getByEmail.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.update.mockResolvedValue(userStub.user1);
 | 
					      userMock.update.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      sessionMock.create.mockResolvedValue(sessionStub.valid);
 | 
					      sessionMock.create.mockResolvedValue(sessionStub.valid);
 | 
				
			||||||
@ -400,7 +400,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should allow auto registering by default', async () => {
 | 
					    it('should allow auto registering by default', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.enabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.enabled);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
					      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.create.mockResolvedValue(userStub.user1);
 | 
					      userMock.create.mockResolvedValue(userStub.user1);
 | 
				
			||||||
@ -415,7 +415,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should use the mobile redirect override', async () => {
 | 
					    it('should use the mobile redirect override', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.override);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.override);
 | 
				
			||||||
      userMock.getByOAuthId.mockResolvedValue(userStub.user1);
 | 
					      userMock.getByOAuthId.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      sessionMock.create.mockResolvedValue(sessionStub.valid);
 | 
					      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 () => {
 | 
					    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);
 | 
					      userMock.getByOAuthId.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      sessionMock.create.mockResolvedValue(sessionStub.valid);
 | 
					      sessionMock.create.mockResolvedValue(sessionStub.valid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -435,7 +435,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should use the default quota', async () => {
 | 
					    it('should use the default quota', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
					      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.create.mockResolvedValue(userStub.user1);
 | 
					      userMock.create.mockResolvedValue(userStub.user1);
 | 
				
			||||||
@ -448,7 +448,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should ignore an invalid storage quota', async () => {
 | 
					    it('should ignore an invalid storage quota', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
					      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.create.mockResolvedValue(userStub.user1);
 | 
					      userMock.create.mockResolvedValue(userStub.user1);
 | 
				
			||||||
@ -462,7 +462,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should ignore a negative quota', async () => {
 | 
					    it('should ignore a negative quota', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
					      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.create.mockResolvedValue(userStub.user1);
 | 
					      userMock.create.mockResolvedValue(userStub.user1);
 | 
				
			||||||
@ -476,7 +476,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not set quota for 0 quota', async () => {
 | 
					    it('should not set quota for 0 quota', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
					      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.create.mockResolvedValue(userStub.user1);
 | 
					      userMock.create.mockResolvedValue(userStub.user1);
 | 
				
			||||||
@ -496,7 +496,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should use a valid storage quota', async () => {
 | 
					    it('should use a valid storage quota', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
 | 
				
			||||||
      userMock.getByEmail.mockResolvedValue(null);
 | 
					      userMock.getByEmail.mockResolvedValue(null);
 | 
				
			||||||
      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
					      userMock.getAdmin.mockResolvedValue(userStub.user1);
 | 
				
			||||||
      userMock.create.mockResolvedValue(userStub.user1);
 | 
					      userMock.create.mockResolvedValue(userStub.user1);
 | 
				
			||||||
@ -518,7 +518,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('link', () => {
 | 
					  describe('link', () => {
 | 
				
			||||||
    it('should link an account', async () => {
 | 
					    it('should link an account', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.enabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.enabled);
 | 
				
			||||||
      userMock.update.mockResolvedValue(userStub.user1);
 | 
					      userMock.update.mockResolvedValue(userStub.user1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' });
 | 
					      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 () => {
 | 
					    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);
 | 
					      userMock.getByOAuthId.mockResolvedValue({ id: 'other-user' } as UserEntity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
 | 
					      await expect(sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' })).rejects.toBeInstanceOf(
 | 
				
			||||||
@ -540,7 +540,7 @@ describe('AuthService', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('unlink', () => {
 | 
					  describe('unlink', () => {
 | 
				
			||||||
    it('should unlink an account', async () => {
 | 
					    it('should unlink an account', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.enabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.enabled);
 | 
				
			||||||
      userMock.update.mockResolvedValue(userStub.user1);
 | 
					      userMock.update.mockResolvedValue(userStub.user1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.unlink(authStub.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 { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
 | 
					import { ISessionRepository } from 'src/interfaces/session.interface';
 | 
				
			||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { HumanReadableSize } from 'src/utils/bytes';
 | 
					import { HumanReadableSize } from 'src/utils/bytes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -67,7 +67,7 @@ export class AuthService {
 | 
				
			|||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAccessRepository) accessRepository: IAccessRepository,
 | 
					    @Inject(IAccessRepository) accessRepository: IAccessRepository,
 | 
				
			||||||
    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
					    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
 | 
					    @Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
@ -77,7 +77,7 @@ export class AuthService {
 | 
				
			|||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(AuthService.name);
 | 
					    this.logger.setContext(AuthService.name);
 | 
				
			||||||
    this.access = AccessCore.create(accessRepository);
 | 
					    this.access = AccessCore.create(accessRepository);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
 | 
				
			||||||
    this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
 | 
					    this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    custom.setHttpOptionsDefaults({ timeout: 30_000 });
 | 
					    custom.setHttpOptionsDefaults({ timeout: 30_000 });
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ import {
 | 
				
			|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
 | 
					import { IMetricRepository } from 'src/interfaces/metric.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.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 { JobService } from 'src/services/job.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
					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 { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock';
 | 
					import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock';
 | 
				
			||||||
import { newPersonRepositoryMock } from 'test/repositories/person.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';
 | 
					import { Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeMockHandlers = (status: JobStatus) => {
 | 
					const makeMockHandlers = (status: JobStatus) => {
 | 
				
			||||||
@ -38,22 +38,22 @@ const makeMockHandlers = (status: JobStatus) => {
 | 
				
			|||||||
describe(JobService.name, () => {
 | 
					describe(JobService.name, () => {
 | 
				
			||||||
  let sut: JobService;
 | 
					  let sut: JobService;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					 | 
				
			||||||
  let eventMock: Mocked<IEventRepository>;
 | 
					  let eventMock: Mocked<IEventRepository>;
 | 
				
			||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let personMock: Mocked<IPersonRepository>;
 | 
					  let personMock: Mocked<IPersonRepository>;
 | 
				
			||||||
  let metricMock: Mocked<IMetricRepository>;
 | 
					  let metricMock: Mocked<IMetricRepository>;
 | 
				
			||||||
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    eventMock = newEventRepositoryMock();
 | 
					    eventMock = newEventRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    personMock = newPersonRepositoryMock();
 | 
					    personMock = newPersonRepositoryMock();
 | 
				
			||||||
    metricMock = newMetricRepositoryMock();
 | 
					    metricMock = newMetricRepositoryMock();
 | 
				
			||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    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', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
@ -234,14 +234,14 @@ describe(JobService.name, () => {
 | 
				
			|||||||
  describe('init', () => {
 | 
					  describe('init', () => {
 | 
				
			||||||
    it('should register a handler for each queue', async () => {
 | 
					    it('should register a handler for each queue', async () => {
 | 
				
			||||||
      await sut.init(makeMockHandlers(JobStatus.SUCCESS));
 | 
					      await sut.init(makeMockHandlers(JobStatus.SUCCESS));
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
 | 
					      expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should subscribe to config changes', async () => {
 | 
					    it('should subscribe to config changes', async () => {
 | 
				
			||||||
      await sut.init(makeMockHandlers(JobStatus.FAILED));
 | 
					      await sut.init(makeMockHandlers(JobStatus.FAILED));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      SystemConfigCore.create(newSystemConfigRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
 | 
					      SystemConfigCore.create(newSystemMetadataRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
 | 
				
			||||||
        job: {
 | 
					        job: {
 | 
				
			||||||
          [QueueName.BACKGROUND_TASK]: { concurrency: 10 },
 | 
					          [QueueName.BACKGROUND_TASK]: { concurrency: 10 },
 | 
				
			||||||
          [QueueName.SMART_SEARCH]: { concurrency: 10 },
 | 
					          [QueueName.SMART_SEARCH]: { concurrency: 10 },
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,7 @@ import {
 | 
				
			|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
 | 
					import { IMetricRepository } from 'src/interfaces/metric.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.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()
 | 
					@Injectable()
 | 
				
			||||||
export class JobService {
 | 
					export class JobService {
 | 
				
			||||||
@ -30,13 +30,13 @@ export class JobService {
 | 
				
			|||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
					    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
					    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
				
			||||||
    @Inject(IMetricRepository) private metricRepository: IMetricRepository,
 | 
					    @Inject(IMetricRepository) private metricRepository: IMetricRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(JobService.name);
 | 
					    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> {
 | 
					  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 { mapLibrary } from 'src/dtos/library.dto';
 | 
				
			||||||
import { AssetType } from 'src/entities/asset.entity';
 | 
					import { AssetType } from 'src/entities/asset.entity';
 | 
				
			||||||
import { LibraryType } from 'src/entities/library.entity';
 | 
					import { LibraryType } from 'src/entities/library.entity';
 | 
				
			||||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
 | 
					 | 
				
			||||||
import { UserEntity } from 'src/entities/user.entity';
 | 
					import { UserEntity } from 'src/entities/user.entity';
 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { ICryptoRepository } from 'src/interfaces/crypto.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 { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { LibraryService } from 'src/services/library.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { authStub } from 'test/fixtures/auth.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 { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
 | 
				
			||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
					import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/storage.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';
 | 
					import { Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(LibraryService.name, () => {
 | 
					describe(LibraryService.name, () => {
 | 
				
			||||||
  let sut: LibraryService;
 | 
					  let sut: LibraryService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let cryptoMock: Mocked<ICryptoRepository>;
 | 
					  let cryptoMock: Mocked<ICryptoRepository>;
 | 
				
			||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let libraryMock: Mocked<ILibraryRepository>;
 | 
					  let libraryMock: Mocked<ILibraryRepository>;
 | 
				
			||||||
@ -44,7 +43,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    libraryMock = newLibraryRepositoryMock();
 | 
					    libraryMock = newLibraryRepositoryMock();
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
@ -55,7 +54,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    sut = new LibraryService(
 | 
					    sut = new LibraryService(
 | 
				
			||||||
      assetMock,
 | 
					      assetMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      cryptoMock,
 | 
					      cryptoMock,
 | 
				
			||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      libraryMock,
 | 
					      libraryMock,
 | 
				
			||||||
@ -73,16 +72,13 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('init', () => {
 | 
					  describe('init', () => {
 | 
				
			||||||
    it('should init cron job and subscribe to config changes', async () => {
 | 
					    it('should init cron job and subscribe to config changes', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
 | 
				
			||||||
        { key: SystemConfigKey.LIBRARY_SCAN_ENABLED, value: true },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.LIBRARY_SCAN_CRON_EXPRESSION, value: '0 0 * * *' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.addCronJob).toHaveBeenCalled();
 | 
					      expect(jobMock.addCronJob).toHaveBeenCalled();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      SystemConfigCore.create(newSystemConfigRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
 | 
					      SystemConfigCore.create(newSystemMetadataRepositoryMock(false), newLoggerRepositoryMock()).config$.next({
 | 
				
			||||||
        library: {
 | 
					        library: {
 | 
				
			||||||
          scan: {
 | 
					          scan: {
 | 
				
			||||||
            enabled: true,
 | 
					            enabled: true,
 | 
				
			||||||
@ -101,7 +97,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
        libraryStub.externalLibraryWithImportPaths2,
 | 
					        libraryStub.externalLibraryWithImportPaths2,
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
      libraryMock.get.mockImplementation((id) =>
 | 
					      libraryMock.get.mockImplementation((id) =>
 | 
				
			||||||
        Promise.resolve(
 | 
					        Promise.resolve(
 | 
				
			||||||
          [libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find(
 | 
					          [libraryStub.externalLibraryWithImportPaths1, libraryStub.externalLibraryWithImportPaths2].find(
 | 
				
			||||||
@ -121,7 +117,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not initialize watcher when watching is disabled', async () => {
 | 
					    it('should not initialize watcher when watching is disabled', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -129,7 +125,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should not initialize watcher when lock is taken', async () => {
 | 
					    it('should not initialize watcher when lock is taken', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
      databaseMock.tryLock.mockResolvedValue(false);
 | 
					      databaseMock.tryLock.mockResolvedValue(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
@ -757,7 +753,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
      libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
 | 
					      libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
 | 
				
			||||||
      libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
 | 
					      libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const mockClose = vitest.fn();
 | 
					      const mockClose = vitest.fn();
 | 
				
			||||||
      storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
 | 
					      storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
 | 
				
			||||||
@ -897,7 +893,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it('should create watched with import paths', async () => {
 | 
					      it('should create watched with import paths', async () => {
 | 
				
			||||||
        configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					        systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
        libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
 | 
					        libraryMock.create.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
 | 
				
			||||||
        libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
 | 
					        libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
 | 
				
			||||||
        libraryMock.getAll.mockResolvedValue([]);
 | 
					        libraryMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
@ -1041,7 +1037,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('update', () => {
 | 
					  describe('update', () => {
 | 
				
			||||||
    beforeEach(async () => {
 | 
					    beforeEach(async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
      libraryMock.getAll.mockResolvedValue([]);
 | 
					      libraryMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.init();
 | 
					      await sut.init();
 | 
				
			||||||
@ -1058,7 +1054,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
  describe('watchAll', () => {
 | 
					  describe('watchAll', () => {
 | 
				
			||||||
    describe('watching disabled', () => {
 | 
					    describe('watching disabled', () => {
 | 
				
			||||||
      beforeEach(async () => {
 | 
					      beforeEach(async () => {
 | 
				
			||||||
        configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
 | 
					        systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await sut.init();
 | 
					        await sut.init();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -1074,7 +1070,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    describe('watching enabled', () => {
 | 
					    describe('watching enabled', () => {
 | 
				
			||||||
      beforeEach(async () => {
 | 
					      beforeEach(async () => {
 | 
				
			||||||
        configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					        systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
        libraryMock.getAll.mockResolvedValue([]);
 | 
					        libraryMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
        await sut.init();
 | 
					        await sut.init();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -1229,7 +1225,7 @@ describe(LibraryService.name, () => {
 | 
				
			|||||||
        libraryStub.externalLibraryWithImportPaths2,
 | 
					        libraryStub.externalLibraryWithImportPaths2,
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
 | 
				
			||||||
      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
 | 
					      libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      libraryMock.get.mockImplementation((id) =>
 | 
					      libraryMock.get.mockImplementation((id) =>
 | 
				
			||||||
 | 
				
			|||||||
@ -38,7 +38,7 @@ import {
 | 
				
			|||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
					import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { mimeTypes } from 'src/utils/mime-types';
 | 
				
			||||||
import { handlePromiseError } from 'src/utils/misc';
 | 
					import { handlePromiseError } from 'src/utils/misc';
 | 
				
			||||||
import { usePagination } from 'src/utils/pagination';
 | 
					import { usePagination } from 'src/utils/pagination';
 | 
				
			||||||
@ -55,7 +55,7 @@ export class LibraryService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
					    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ILibraryRepository) private repository: ILibraryRepository,
 | 
					    @Inject(ILibraryRepository) private repository: ILibraryRepository,
 | 
				
			||||||
@ -64,7 +64,7 @@ export class LibraryService {
 | 
				
			|||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(LibraryService.name);
 | 
					    this.logger.setContext(LibraryService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async init() {
 | 
					  async init() {
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,6 @@ import {
 | 
				
			|||||||
} from 'src/config';
 | 
					} from 'src/config';
 | 
				
			||||||
import { AssetType } from 'src/entities/asset.entity';
 | 
					import { AssetType } from 'src/entities/asset.entity';
 | 
				
			||||||
import { ExifEntity } from 'src/entities/exif.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 { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 | 
					import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 | 
				
			||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { MediaService } from 'src/services/media.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { faceStub } from 'test/fixtures/face.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 { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
 | 
				
			||||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
					import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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';
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(MediaService.name, () => {
 | 
					describe(MediaService.name, () => {
 | 
				
			||||||
  let sut: MediaService;
 | 
					  let sut: MediaService;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					 | 
				
			||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let mediaMock: Mocked<IMediaRepository>;
 | 
					  let mediaMock: Mocked<IMediaRepository>;
 | 
				
			||||||
  let moveMock: Mocked<IMoveRepository>;
 | 
					  let moveMock: Mocked<IMoveRepository>;
 | 
				
			||||||
  let personMock: Mocked<IPersonRepository>;
 | 
					  let personMock: Mocked<IPersonRepository>;
 | 
				
			||||||
  let storageMock: Mocked<IStorageRepository>;
 | 
					  let storageMock: Mocked<IStorageRepository>;
 | 
				
			||||||
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let cryptoMock: Mocked<ICryptoRepository>;
 | 
					  let cryptoMock: Mocked<ICryptoRepository>;
 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    mediaMock = newMediaRepositoryMock();
 | 
					    mediaMock = newMediaRepositoryMock();
 | 
				
			||||||
    moveMock = newMoveRepositoryMock();
 | 
					    moveMock = newMoveRepositoryMock();
 | 
				
			||||||
@ -65,7 +64,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      mediaMock,
 | 
					      mediaMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      moveMock,
 | 
					      moveMock,
 | 
				
			||||||
      cryptoMock,
 | 
					      cryptoMock,
 | 
				
			||||||
      loggerMock,
 | 
					      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) => {
 | 
					    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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
      const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`;
 | 
					      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 () => {
 | 
					    it('should delete previous preview if different path', async () => {
 | 
				
			||||||
      const previousPreviewPath = assetStub.image.previewPath;
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleGeneratePreview({ id: assetStub.image.id });
 | 
					      await sut.handleGeneratePreview({ id: assetStub.image.id });
 | 
				
			||||||
@ -337,10 +336,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should always generate video thumbnail in one pass', async () => {
 | 
					    it('should always generate video thumbnail in one pass', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
 | 
					        ffmpeg: { twoPass: true, maxBitrate: '5000k' },
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '5000k' },
 | 
					      });
 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleGeneratePreview({ id: assetStub.video.id });
 | 
					      await sut.handleGeneratePreview({ id: assetStub.video.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -385,7 +383,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it.each(Object.values(ImageFormat))(
 | 
					    it.each(Object.values(ImageFormat))(
 | 
				
			||||||
      'should generate a %s thumbnail for an image when specified',
 | 
					      'should generate a %s thumbnail for an image when specified',
 | 
				
			||||||
      async (format) => {
 | 
					      async (format) => {
 | 
				
			||||||
        configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_THUMBNAIL_FORMAT, value: format }]);
 | 
					        systemMock.get.mockResolvedValue({ image: { thumbnailFormat: format } });
 | 
				
			||||||
        assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					        assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
        const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`;
 | 
					        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 () => {
 | 
					    it('should delete previous thumbnail if different path', async () => {
 | 
				
			||||||
      const previousThumbnailPath = assetStub.image.thumbnailPath;
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
					      await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
				
			||||||
@ -438,7 +436,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
  it('should extract embedded image if enabled and available', async () => {
 | 
					  it('should extract embedded image if enabled and available', async () => {
 | 
				
			||||||
    mediaMock.extract.mockResolvedValue(true);
 | 
					    mediaMock.extract.mockResolvedValue(true);
 | 
				
			||||||
    mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
 | 
					    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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
					    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 () => {
 | 
					  it('should resize original image if embedded image is too small', async () => {
 | 
				
			||||||
    mediaMock.extract.mockResolvedValue(true);
 | 
					    mediaMock.extract.mockResolvedValue(true);
 | 
				
			||||||
    mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
 | 
					    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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
					    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 () => {
 | 
					  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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
					    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 () => {
 | 
					  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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
					    await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 | 
				
			||||||
@ -626,7 +624,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext');
 | 
					      expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext');
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
      expect(storageMock.mkdirSync).toHaveBeenCalled();
 | 
					      expect(storageMock.mkdirSync).toHaveBeenCalled();
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.ext',
 | 
				
			||||||
@ -655,7 +653,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should transcode when set to all', async () => {
 | 
					    it('should transcode when set to all', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -671,7 +669,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should transcode when optimal and too big', async () => {
 | 
					    it('should transcode when optimal and too big', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      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 });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.ext',
 | 
				
			||||||
@ -686,10 +684,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should transcode when policy Bitrate and bitrate higher than max bitrate', async () => {
 | 
					    it('should transcode when policy Bitrate and bitrate higher than max bitrate', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStream40Mbps);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.BITRATE, maxBitrate: '30M' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.BITRATE },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '30M' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.ext',
 | 
				
			||||||
@ -704,10 +699,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should not scale resolution if no target resolution', async () => {
 | 
					    it('should not scale resolution if no target resolution', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.ext',
 | 
				
			||||||
@ -722,7 +714,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should scale horizontally when video is horizontal', async () => {
 | 
					    it('should scale horizontally when video is horizontal', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -738,7 +730,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should scale vertically when video is vertical', async () => {
 | 
					    it('should scale vertically when video is vertical', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -754,10 +746,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should always scale video if height is uneven', async () => {
 | 
					    it('should always scale video if height is uneven', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddHeight);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddHeight);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -773,10 +762,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should always scale video if width is uneven', async () => {
 | 
					    it('should always scale video if width is uneven', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddWidth);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamOddWidth);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.ALL },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: 'original' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -792,10 +778,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should copy video stream when video matches target', async () => {
 | 
					    it('should copy video stream when video matches target', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					        ffmpeg: { targetVideoCodec: VideoCodec.HEVC, acceptedAudioCodecs: [AudioCodec.AAC] },
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
 | 
					      });
 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    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);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamH264);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEPTED_VIDEO_CODECS, value: [VideoCodec.H264, VideoCodec.HEVC] },
 | 
					          targetVideoCodec: VideoCodec.HEVC,
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
 | 
					          acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.HEVC],
 | 
				
			||||||
      ]);
 | 
					          acceptedAudioCodecs: [AudioCodec.AAC],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    it('should include hevc tag when target is hevc and copying hevc video stream', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEPTED_VIDEO_CODECS, value: [VideoCodec.H264, VideoCodec.HEVC] },
 | 
					          targetVideoCodec: VideoCodec.HEVC,
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
 | 
					          acceptedVideoCodecs: [VideoCodec.H264, VideoCodec.HEVC],
 | 
				
			||||||
      ]);
 | 
					          acceptedAudioCodecs: [AudioCodec.AAC],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -851,7 +840,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should copy audio stream when audio matches target', async () => {
 | 
					    it('should copy audio stream when audio matches target', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.audioStreamAac);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -867,7 +856,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should throw an exception if transcode value is invalid', async () => {
 | 
					    it('should throw an exception if transcode value is invalid', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      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();
 | 
					      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrow();
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -875,7 +864,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should not transcode if transcoding is disabled', async () => {
 | 
					    it('should not transcode if transcoding is disabled', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -883,7 +872,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should not transcode if target codec is invalid', async () => {
 | 
					    it('should not transcode if target codec is invalid', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      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 () => {
 | 
					    it('should delete existing transcode if current policy does not require transcoding', async () => {
 | 
				
			||||||
      const asset = assetStub.hasEncodedVideo;
 | 
					      const asset = assetStub.hasEncodedVideo;
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([asset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleVideoConversion({ id: asset.id });
 | 
					      await sut.handleVideoConversion({ id: asset.id });
 | 
				
			||||||
@ -906,7 +895,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set max bitrate if above 0', async () => {
 | 
					    it('should set max bitrate if above 0', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '4500k' },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    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);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    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);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '4500k' },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
 | 
					          maxBitrate: '4500k',
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					          twoPass: true,
 | 
				
			||||||
      ]);
 | 
					          targetVideoCodec: VideoCodec.VP9,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    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);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
 | 
					          maxBitrate: '0',
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					          twoPass: true,
 | 
				
			||||||
      ]);
 | 
					          targetVideoCodec: VideoCodec.VP9,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -997,10 +987,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should configure preset for vp9', async () => {
 | 
					    it('should configure preset for vp9', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, preset: 'slow' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PRESET, value: 'slow' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1016,10 +1003,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should not configure preset for vp9 if invalid', async () => {
 | 
					    it('should not configure preset for vp9 if invalid', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.VP9 } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1035,10 +1019,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should configure threads if above 0', async () => {
 | 
					    it('should configure threads if above 0', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, threads: 2 } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_THREADS, value: 2 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1054,7 +1035,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should disable thread pooling for h264 if thread limit is 1', async () => {
 | 
					    it('should disable thread pooling for h264 if thread limit is 1', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    it('should omit thread flags for h264 if thread limit is at or below 0', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1086,10 +1067,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should disable thread pooling for hevc if thread limit is 1', async () => {
 | 
					    it('should disable thread pooling for hevc if thread limit is 1', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.HEVC } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_THREADS, value: 1 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    it('should omit thread flags for hevc if thread limit is at or below 0', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.HEVC } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_THREADS, value: 0 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1124,7 +1099,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should use av1 if specified', async () => {
 | 
					    it('should use av1 if specified', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1150,10 +1125,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should map `veryslow` preset to 4 for av1', async () => {
 | 
					    it('should map `veryslow` preset to 4 for av1', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, preset: 'veryslow' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PRESET, value: 'veryslow' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1169,10 +1141,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set max bitrate for av1 if specified', async () => {
 | 
					    it('should set max bitrate for av1 if specified', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, maxBitrate: '2M' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '2M' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1188,10 +1157,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set threads for av1 if specified', async () => {
 | 
					    it('should set threads for av1 if specified', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4 } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_THREADS, value: 4 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1207,11 +1173,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set both bitrate and threads for av1 if specified', async () => {
 | 
					    it('should set both bitrate and threads for av1 if specified', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4, maxBitrate: '2M' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.AV1 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_THREADS, value: 4 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '2M' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    it('should skip transcoding for audioless videos with optimal policy if video codec is correct', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TRANSCODE, value: TranscodePolicy.OPTIMAL },
 | 
					          targetVideoCodec: VideoCodec.HEVC,
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_RESOLUTION, value: '1080p' },
 | 
					          transcode: TranscodePolicy.OPTIMAL,
 | 
				
			||||||
      ]);
 | 
					          targetResolution: '1080p',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -1239,10 +1203,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should fail if hwaccel is enabled for an unsupported codec', async () => {
 | 
					    it('should fail if hwaccel is enabled for an unsupported codec', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
					      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -1250,7 +1211,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should fail if hwaccel option is invalid', async () => {
 | 
					    it('should fail if hwaccel option is invalid', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
					      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -1258,7 +1219,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set options for nvenc', async () => {
 | 
					    it('should set options for nvenc', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1290,11 +1251,13 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set two pass options for nvenc when enabled', async () => {
 | 
					    it('should set two pass options for nvenc when enabled', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					          accel: TranscodeHWAccel.NVENC,
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TWO_PASS, value: true },
 | 
					          maxBitrate: '10000k',
 | 
				
			||||||
      ]);
 | 
					          twoPass: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1310,10 +1273,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set vbr options for nvenc when max bitrate is enabled', async () => {
 | 
					    it('should set vbr options for nvenc when max bitrate is enabled', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1329,10 +1289,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should set cq options for nvenc when max bitrate is disabled', async () => {
 | 
					    it('should set cq options for nvenc when max bitrate is disabled', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1348,10 +1305,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should omit preset for nvenc if invalid', async () => {
 | 
					    it('should omit preset for nvenc if invalid', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, preset: 'invalid' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1367,7 +1321,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should ignore two pass for nvenc if max bitrate is disabled', async () => {
 | 
					    it('should ignore two pass for nvenc if max bitrate is disabled', async () => {
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1384,10 +1338,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set options for qsv', async () => {
 | 
					    it('should set options for qsv', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1420,11 +1371,13 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set options for qsv with custom dri node', async () => {
 | 
					    it('should set options for qsv with custom dri node', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					          accel: TranscodeHWAccel.QSV,
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PREFERRED_HW_DEVICE, value: '/dev/dri/renderD128' },
 | 
					          maxBitrate: '10000k',
 | 
				
			||||||
      ]);
 | 
					          preferredHwDevice: '/dev/dri/renderD128',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1444,10 +1397,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should omit preset for qsv if invalid', async () => {
 | 
					    it('should omit preset for qsv if invalid', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      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 () => {
 | 
					    it('should set low power mode for qsv if target video codec is vp9', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1484,7 +1431,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should fail for qsv if no hw devices', async () => {
 | 
					    it('should fail for qsv if no hw devices', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue([]);
 | 
					      storageMock.readdir.mockResolvedValue([]);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
					      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -1493,7 +1440,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set options for vaapi', async () => {
 | 
					    it('should set options for vaapi', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1525,10 +1472,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set vbr options for vaapi when max bitrate is enabled', async () => {
 | 
					    it('should set vbr options for vaapi when max bitrate is enabled', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1554,7 +1498,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set cq options for vaapi when max bitrate is disabled', async () => {
 | 
					    it('should set cq options for vaapi when max bitrate is disabled', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1580,10 +1524,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should omit preset for vaapi if invalid', async () => {
 | 
					    it('should omit preset for vaapi if invalid', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PRESET, value: 'invalid' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1603,7 +1544,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should prefer gpu for vaapi if available', async () => {
 | 
					    it('should prefer gpu for vaapi if available', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1623,7 +1564,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should prefer higher index gpu node', async () => {
 | 
					    it('should prefer higher index gpu node', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1643,10 +1584,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should select specific gpu node if selected', async () => {
 | 
					    it('should select specific gpu node if selected', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI },
 | 
					        ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' },
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_PREFERRED_HW_DEVICE, value: '/dev/dri/renderD128' },
 | 
					      });
 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1666,7 +1606,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should fallback to sw transcoding if hw transcoding fails', async () => {
 | 
					    it('should fallback to sw transcoding if hw transcoding fails', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      mediaMock.transcode.mockRejectedValueOnce(new Error('error'));
 | 
					      mediaMock.transcode.mockRejectedValueOnce(new Error('error'));
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
@ -1685,7 +1625,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should fail for vaapi if no hw devices', async () => {
 | 
					    it('should fail for vaapi if no hw devices', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue([]);
 | 
					      storageMock.readdir.mockResolvedValue([]);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
					      await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
 | 
				
			||||||
      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.transcode).not.toHaveBeenCalled();
 | 
				
			||||||
@ -1694,7 +1634,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set options for rkmpp', async () => {
 | 
					    it('should set options for rkmpp', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      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]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1724,11 +1664,13 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
 | 
					    it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
 | 
					        ffmpeg: {
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
 | 
					          accel: TranscodeHWAccel.RKMPP,
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
 | 
					          maxBitrate: '10000k',
 | 
				
			||||||
      ]);
 | 
					          targetVideoCodec: VideoCodec.HEVC,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1745,11 +1687,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
    it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
 | 
					    it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
 | 
				
			||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
 | 
					        ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' },
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_CRF, value: 30 },
 | 
					      });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1767,11 +1707,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
					      storageMock.readdir.mockResolvedValue(['renderD128']);
 | 
				
			||||||
      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
 | 
					      storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
 | 
				
			||||||
      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
					      mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, crf: 30, maxBitrate: '0' } });
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_CRF, value: 30 },
 | 
					 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					      await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1792,7 +1728,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  it('should tonemap when policy is required and video is hdr', async () => {
 | 
					  it('should tonemap when policy is required and video is hdr', async () => {
 | 
				
			||||||
    mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
					    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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
    await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					    await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
    expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					    expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
@ -1812,7 +1748,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  it('should tonemap when policy is optimal and video is hdr', async () => {
 | 
					  it('should tonemap when policy is optimal and video is hdr', async () => {
 | 
				
			||||||
    mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
					    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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
    await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					    await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
    expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					    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 () => {
 | 
					  it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
 | 
				
			||||||
    mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
 | 
					    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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.video]);
 | 
				
			||||||
    await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
					    await sut.handleVideoConversion({ id: assetStub.video.id });
 | 
				
			||||||
    expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
					    expect(mediaMock.transcode).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
				
			|||||||
@ -31,7 +31,7 @@ import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo
 | 
				
			|||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
 | 
					import { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 {
 | 
					import {
 | 
				
			||||||
  AV1Config,
 | 
					  AV1Config,
 | 
				
			||||||
  H264Config,
 | 
					  H264Config,
 | 
				
			||||||
@ -59,20 +59,20 @@ export class MediaService {
 | 
				
			|||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
 | 
					    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
					    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
				
			||||||
    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
 | 
					    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(MediaService.name);
 | 
					    this.logger.setContext(MediaService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
    this.storageCore = StorageCore.create(
 | 
					    this.storageCore = StorageCore.create(
 | 
				
			||||||
      assetRepository,
 | 
					      assetRepository,
 | 
				
			||||||
      cryptoRepository,
 | 
					      cryptoRepository,
 | 
				
			||||||
      moveRepository,
 | 
					      moveRepository,
 | 
				
			||||||
      personRepository,
 | 
					      personRepository,
 | 
				
			||||||
      storageRepository,
 | 
					      storageRepository,
 | 
				
			||||||
      configRepository,
 | 
					      systemMetadataRepository,
 | 
				
			||||||
      this.logger,
 | 
					      this.logger,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -329,7 +329,6 @@ export class MediaService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { ffmpeg: config } = await this.configCore.getConfig();
 | 
					    const { ffmpeg: config } = await this.configCore.getConfig();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const target = this.getTranscodeTarget(config, mainVideoStream, mainAudioStream);
 | 
					    const target = this.getTranscodeTarget(config, mainVideoStream, mainAudioStream);
 | 
				
			||||||
    if (target === TranscodeTarget.NONE) {
 | 
					    if (target === TranscodeTarget.NONE) {
 | 
				
			||||||
      if (asset.encodedVideoPath) {
 | 
					      if (asset.encodedVideoPath) {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ import { Stats } from 'node:fs';
 | 
				
			|||||||
import { constants } from 'node:fs/promises';
 | 
					import { constants } from 'node:fs/promises';
 | 
				
			||||||
import { AssetType } from 'src/entities/asset.entity';
 | 
					import { AssetType } from 'src/entities/asset.entity';
 | 
				
			||||||
import { ExifEntity } from 'src/entities/exif.entity';
 | 
					import { ExifEntity } from 'src/entities/exif.entity';
 | 
				
			||||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
 | 
					 | 
				
			||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
					import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { ICryptoRepository } from 'src/interfaces/crypto.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 { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { MetadataService, Orientation } from 'src/services/metadata.service';
 | 
					import { MetadataService, Orientation } from 'src/services/metadata.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					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 { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
 | 
				
			||||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
					import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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 { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
import { Mocked } from 'vitest';
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(MetadataService.name, () => {
 | 
					describe(MetadataService.name, () => {
 | 
				
			||||||
  let albumMock: Mocked<IAlbumRepository>;
 | 
					  let albumMock: Mocked<IAlbumRepository>;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let cryptoRepository: Mocked<ICryptoRepository>;
 | 
					  let cryptoRepository: Mocked<ICryptoRepository>;
 | 
				
			||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let metadataMock: Mocked<IMetadataRepository>;
 | 
					  let metadataMock: Mocked<IMetadataRepository>;
 | 
				
			||||||
@ -59,7 +58,6 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    albumMock = newAlbumRepositoryMock();
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					 | 
				
			||||||
    cryptoRepository = newCryptoRepositoryMock();
 | 
					    cryptoRepository = newCryptoRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    metadataMock = newMetadataRepositoryMock();
 | 
					    metadataMock = newMetadataRepositoryMock();
 | 
				
			||||||
@ -67,6 +65,7 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
    personMock = newPersonRepositoryMock();
 | 
					    personMock = newPersonRepositoryMock();
 | 
				
			||||||
    eventMock = newEventRepositoryMock();
 | 
					    eventMock = newEventRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    mediaMock = newMediaRepositoryMock();
 | 
					    mediaMock = newMediaRepositoryMock();
 | 
				
			||||||
    databaseMock = newDatabaseRepositoryMock();
 | 
					    databaseMock = newDatabaseRepositoryMock();
 | 
				
			||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
@ -84,7 +83,7 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
      moveMock,
 | 
					      moveMock,
 | 
				
			||||||
      personMock,
 | 
					      personMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      userMock,
 | 
					      userMock,
 | 
				
			||||||
      loggerMock,
 | 
					      loggerMock,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -108,7 +107,7 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should return if reverse geocoding is disabled', async () => {
 | 
					    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();
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -297,7 +296,7 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('should apply reverse geocoding', async () => {
 | 
					    it('should apply reverse geocoding', async () => {
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
 | 
					      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.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
 | 
				
			||||||
      metadataMock.readTags.mockResolvedValue({
 | 
					      metadataMock.readTags.mockResolvedValue({
 | 
				
			||||||
        GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
 | 
					        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 { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { handlePromiseError } from 'src/utils/misc';
 | 
					import { handlePromiseError } from 'src/utils/misc';
 | 
				
			||||||
import { usePagination } from 'src/utils/pagination';
 | 
					import { usePagination } from 'src/utils/pagination';
 | 
				
			||||||
@ -113,19 +113,19 @@ export class MetadataService {
 | 
				
			|||||||
    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
					    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
				
			||||||
    @Inject(IPersonRepository) personRepository: IPersonRepository,
 | 
					    @Inject(IPersonRepository) personRepository: IPersonRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(MetadataService.name);
 | 
					    this.logger.setContext(MetadataService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
    this.storageCore = StorageCore.create(
 | 
					    this.storageCore = StorageCore.create(
 | 
				
			||||||
      assetRepository,
 | 
					      assetRepository,
 | 
				
			||||||
      cryptoRepository,
 | 
					      cryptoRepository,
 | 
				
			||||||
      moveRepository,
 | 
					      moveRepository,
 | 
				
			||||||
      personRepository,
 | 
					      personRepository,
 | 
				
			||||||
      storageRepository,
 | 
					      storageRepository,
 | 
				
			||||||
      configRepository,
 | 
					      systemMetadataRepository,
 | 
				
			||||||
      this.logger,
 | 
					      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 { IEmailJob, IJobRepository, INotifySignupJob, JobName, JobStatus } from 'src/interfaces/job.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.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';
 | 
					import { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
@ -13,14 +13,14 @@ export class NotificationService {
 | 
				
			|||||||
  private configCore: SystemConfigCore;
 | 
					  private configCore: SystemConfigCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(INotificationRepository) private notificationRepository: INotificationRepository,
 | 
					    @Inject(INotificationRepository) private notificationRepository: INotificationRepository,
 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(NotificationService.name);
 | 
					    this.logger.setContext(NotificationService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  init() {
 | 
					  init() {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ import { Colorspace } from 'src/config';
 | 
				
			|||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 | 
					import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
 | 
				
			||||||
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
 | 
					import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
 | 
				
			||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 | 
					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 { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 | 
					import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 | 
				
			||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
 | 
					import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { PersonService } from 'src/services/person.service';
 | 
				
			||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
					import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { authStub } from 'test/fixtures/auth.stub';
 | 
					import { authStub } from 'test/fixtures/auth.stub';
 | 
				
			||||||
import { faceStub } from 'test/fixtures/face.stub';
 | 
					import { faceStub } from 'test/fixtures/face.stub';
 | 
				
			||||||
import { personStub } from 'test/fixtures/person.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 { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
 | 
				
			||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
					import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
				
			||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.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 { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
				
			||||||
import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
 | 
					import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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 { IsNull } from 'typeorm';
 | 
				
			||||||
import { Mocked } from 'vitest';
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -64,7 +64,7 @@ const detectFaceMock = {
 | 
				
			|||||||
describe(PersonService.name, () => {
 | 
					describe(PersonService.name, () => {
 | 
				
			||||||
  let accessMock: IAccessRepositoryMock;
 | 
					  let accessMock: IAccessRepositoryMock;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let machineLearningMock: Mocked<IMachineLearningRepository>;
 | 
					  let machineLearningMock: Mocked<IMachineLearningRepository>;
 | 
				
			||||||
  let mediaMock: Mocked<IMediaRepository>;
 | 
					  let mediaMock: Mocked<IMediaRepository>;
 | 
				
			||||||
@ -79,7 +79,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    accessMock = newAccessRepositoryMock();
 | 
					    accessMock = newAccessRepositoryMock();
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    machineLearningMock = newMachineLearningRepositoryMock();
 | 
					    machineLearningMock = newMachineLearningRepositoryMock();
 | 
				
			||||||
    moveMock = newMoveRepositoryMock();
 | 
					    moveMock = newMoveRepositoryMock();
 | 
				
			||||||
@ -96,7 +96,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
      moveMock,
 | 
					      moveMock,
 | 
				
			||||||
      mediaMock,
 | 
					      mediaMock,
 | 
				
			||||||
      personMock,
 | 
					      personMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      searchMock,
 | 
					      searchMock,
 | 
				
			||||||
@ -451,12 +451,12 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleQueueDetectFaces', () => {
 | 
					  describe('handleQueueDetectFaces', () => {
 | 
				
			||||||
    it('should skip if machine learning is disabled', async () => {
 | 
					    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);
 | 
					      await expect(sut.handleQueueDetectFaces({})).resolves.toBe(JobStatus.SKIPPED);
 | 
				
			||||||
      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
					      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.queueAll).not.toHaveBeenCalled();
 | 
					      expect(jobMock.queueAll).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should queue missing assets', async () => {
 | 
					    it('should queue missing assets', async () => {
 | 
				
			||||||
@ -528,11 +528,11 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
  describe('handleQueueRecognizeFaces', () => {
 | 
					  describe('handleQueueRecognizeFaces', () => {
 | 
				
			||||||
    it('should skip if machine learning is disabled', async () => {
 | 
					    it('should skip if machine learning is disabled', async () => {
 | 
				
			||||||
      jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
 | 
					      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);
 | 
					      await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED);
 | 
				
			||||||
      expect(jobMock.queueAll).not.toHaveBeenCalled();
 | 
					      expect(jobMock.queueAll).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip if recognition jobs are already queued', async () => {
 | 
					    it('should skip if recognition jobs are already queued', async () => {
 | 
				
			||||||
@ -609,11 +609,11 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleDetectFaces', () => {
 | 
					  describe('handleDetectFaces', () => {
 | 
				
			||||||
    it('should skip if machine learning is disabled', async () => {
 | 
					    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);
 | 
					      await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(JobStatus.SKIPPED);
 | 
				
			||||||
      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
					      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip when no resize path', async () => {
 | 
					    it('should skip when no resize path', async () => {
 | 
				
			||||||
@ -740,9 +740,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
        { face: faceStub.face1, distance: 0.4 },
 | 
					        { face: faceStub.face1, distance: 0.4 },
 | 
				
			||||||
      ] as FaceSearchResult[];
 | 
					      ] as FaceSearchResult[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
 | 
				
			||||||
        { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      searchMock.searchFaces.mockResolvedValue(faces);
 | 
					      searchMock.searchFaces.mockResolvedValue(faces);
 | 
				
			||||||
      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
					      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
				
			||||||
      personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
 | 
					      personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
 | 
				
			||||||
@ -767,9 +765,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
        { face: faceStub.noPerson2, distance: 0.3 },
 | 
					        { face: faceStub.noPerson2, distance: 0.3 },
 | 
				
			||||||
      ] as FaceSearchResult[];
 | 
					      ] as FaceSearchResult[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
 | 
				
			||||||
        { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 1 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      searchMock.searchFaces.mockResolvedValue(faces);
 | 
					      searchMock.searchFaces.mockResolvedValue(faces);
 | 
				
			||||||
      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
					      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
				
			||||||
      personMock.create.mockResolvedValue(personStub.withName);
 | 
					      personMock.create.mockResolvedValue(personStub.withName);
 | 
				
			||||||
@ -807,9 +803,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
        { face: faceStub.noPerson2, distance: 0.4 },
 | 
					        { face: faceStub.noPerson2, distance: 0.4 },
 | 
				
			||||||
      ] as FaceSearchResult[];
 | 
					      ] as FaceSearchResult[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
 | 
				
			||||||
        { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      searchMock.searchFaces.mockResolvedValue(faces);
 | 
					      searchMock.searchFaces.mockResolvedValue(faces);
 | 
				
			||||||
      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
					      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
				
			||||||
      personMock.create.mockResolvedValue(personStub.withName);
 | 
					      personMock.create.mockResolvedValue(personStub.withName);
 | 
				
			||||||
@ -831,9 +825,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
        { face: faceStub.noPerson2, distance: 0.4 },
 | 
					        { face: faceStub.noPerson2, distance: 0.4 },
 | 
				
			||||||
      ] as FaceSearchResult[];
 | 
					      ] as FaceSearchResult[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
 | 
				
			||||||
        { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 3 },
 | 
					 | 
				
			||||||
      ]);
 | 
					 | 
				
			||||||
      searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
 | 
					      searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
 | 
				
			||||||
      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
					      personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
 | 
				
			||||||
      personMock.create.mockResolvedValue(personStub.withName);
 | 
					      personMock.create.mockResolvedValue(personStub.withName);
 | 
				
			||||||
@ -849,11 +841,11 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleGeneratePersonThumbnail', () => {
 | 
					  describe('handleGeneratePersonThumbnail', () => {
 | 
				
			||||||
    it('should skip if machine learning is disabled', async () => {
 | 
					    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);
 | 
					      await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.SKIPPED);
 | 
				
			||||||
      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
					      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip a person not found', async () => {
 | 
					    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 { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { ISearchRepository } from 'src/interfaces/search.interface';
 | 
					import { ISearchRepository } from 'src/interfaces/search.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { Orientation } from 'src/services/metadata.service';
 | 
				
			||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
					import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
				
			||||||
import { mimeTypes } from 'src/utils/mime-types';
 | 
					import { mimeTypes } from 'src/utils/mime-types';
 | 
				
			||||||
@ -66,7 +66,7 @@ export class PersonService {
 | 
				
			|||||||
    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
					    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
				
			||||||
    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
 | 
					    @Inject(IMediaRepository) private mediaRepository: IMediaRepository,
 | 
				
			||||||
    @Inject(IPersonRepository) private repository: IPersonRepository,
 | 
					    @Inject(IPersonRepository) private repository: IPersonRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
 | 
					    @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
 | 
				
			||||||
@ -75,14 +75,14 @@ export class PersonService {
 | 
				
			|||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.access = AccessCore.create(accessRepository);
 | 
					    this.access = AccessCore.create(accessRepository);
 | 
				
			||||||
    this.logger.setContext(PersonService.name);
 | 
					    this.logger.setContext(PersonService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
    this.storageCore = StorageCore.create(
 | 
					    this.storageCore = StorageCore.create(
 | 
				
			||||||
      assetRepository,
 | 
					      assetRepository,
 | 
				
			||||||
      cryptoRepository,
 | 
					      cryptoRepository,
 | 
				
			||||||
      moveRepository,
 | 
					      moveRepository,
 | 
				
			||||||
      repository,
 | 
					      repository,
 | 
				
			||||||
      storageRepository,
 | 
					      storageRepository,
 | 
				
			||||||
      configRepository,
 | 
					      systemMetadataRepository,
 | 
				
			||||||
      this.logger,
 | 
					      this.logger,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import { IMetadataRepository } from 'src/interfaces/metadata.interface';
 | 
				
			|||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
					import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { ISearchRepository } from 'src/interfaces/search.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 { SearchService } from 'src/services/search.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { authStub } from 'test/fixtures/auth.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 { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
 | 
				
			||||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
					import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
				
			||||||
import { newSearchRepositoryMock } from 'test/repositories/search.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';
 | 
					import { Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
vitest.useFakeTimers();
 | 
					vitest.useFakeTimers();
 | 
				
			||||||
@ -26,7 +26,7 @@ vitest.useFakeTimers();
 | 
				
			|||||||
describe(SearchService.name, () => {
 | 
					describe(SearchService.name, () => {
 | 
				
			||||||
  let sut: SearchService;
 | 
					  let sut: SearchService;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let machineMock: Mocked<IMachineLearningRepository>;
 | 
					  let machineMock: Mocked<IMachineLearningRepository>;
 | 
				
			||||||
  let personMock: Mocked<IPersonRepository>;
 | 
					  let personMock: Mocked<IPersonRepository>;
 | 
				
			||||||
  let searchMock: Mocked<ISearchRepository>;
 | 
					  let searchMock: Mocked<ISearchRepository>;
 | 
				
			||||||
@ -36,7 +36,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    machineMock = newMachineLearningRepositoryMock();
 | 
					    machineMock = newMachineLearningRepositoryMock();
 | 
				
			||||||
    personMock = newPersonRepositoryMock();
 | 
					    personMock = newPersonRepositoryMock();
 | 
				
			||||||
    searchMock = newSearchRepositoryMock();
 | 
					    searchMock = newSearchRepositoryMock();
 | 
				
			||||||
@ -45,7 +45,7 @@ describe(SearchService.name, () => {
 | 
				
			|||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    loggerMock = newLoggerRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new SearchService(
 | 
					    sut = new SearchService(
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      machineMock,
 | 
					      machineMock,
 | 
				
			||||||
      personMock,
 | 
					      personMock,
 | 
				
			||||||
      searchMock,
 | 
					      searchMock,
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@ import { IMetadataRepository } from 'src/interfaces/metadata.interface';
 | 
				
			|||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
					import { IPartnerRepository } from 'src/interfaces/partner.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.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';
 | 
					import { isSmartSearchEnabled } from 'src/utils/misc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
@ -31,7 +31,7 @@ export class SearchService {
 | 
				
			|||||||
  private configCore: SystemConfigCore;
 | 
					  private configCore: SystemConfigCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
					    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
				
			||||||
    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
					    @Inject(IPersonRepository) private personRepository: IPersonRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
					    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
				
			||||||
@ -41,7 +41,7 @@ export class SearchService {
 | 
				
			|||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(SearchService.name);
 | 
					    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[]> {
 | 
					  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 { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 | 
					import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
				
			||||||
import { IUserRepository } from 'src/interfaces/user.interface';
 | 
					import { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { ServerInfoService } from 'src/services/server-info.service';
 | 
					import { ServerInfoService } from 'src/services/server-info.service';
 | 
				
			||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
 | 
					import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
 | 
				
			||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
					import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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 { newServerInfoRepositoryMock } from 'test/repositories/system-info.repository.mock';
 | 
				
			||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
 | 
					import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
 | 
				
			||||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
					import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
@ -19,31 +17,21 @@ import { Mocked } from 'vitest';
 | 
				
			|||||||
describe(ServerInfoService.name, () => {
 | 
					describe(ServerInfoService.name, () => {
 | 
				
			||||||
  let sut: ServerInfoService;
 | 
					  let sut: ServerInfoService;
 | 
				
			||||||
  let eventMock: Mocked<IEventRepository>;
 | 
					  let eventMock: Mocked<IEventRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					 | 
				
			||||||
  let serverInfoMock: Mocked<IServerInfoRepository>;
 | 
					  let serverInfoMock: Mocked<IServerInfoRepository>;
 | 
				
			||||||
  let storageMock: Mocked<IStorageRepository>;
 | 
					  let storageMock: Mocked<IStorageRepository>;
 | 
				
			||||||
  let userMock: Mocked<IUserRepository>;
 | 
					  let userMock: Mocked<IUserRepository>;
 | 
				
			||||||
  let systemMetadataMock: Mocked<ISystemMetadataRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					 | 
				
			||||||
    eventMock = newEventRepositoryMock();
 | 
					    eventMock = newEventRepositoryMock();
 | 
				
			||||||
    serverInfoMock = newServerInfoRepositoryMock();
 | 
					    serverInfoMock = newServerInfoRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
    systemMetadataMock = newSystemMetadataRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    loggerMock = newLoggerRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new ServerInfoService(
 | 
					    sut = new ServerInfoService(eventMock, userMock, serverInfoMock, storageMock, systemMock, loggerMock);
 | 
				
			||||||
      eventMock,
 | 
					 | 
				
			||||||
      configMock,
 | 
					 | 
				
			||||||
      userMock,
 | 
					 | 
				
			||||||
      serverInfoMock,
 | 
					 | 
				
			||||||
      storageMock,
 | 
					 | 
				
			||||||
      systemMetadataMock,
 | 
					 | 
				
			||||||
      loggerMock,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
@ -188,7 +176,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
        trash: true,
 | 
					        trash: true,
 | 
				
			||||||
        email: false,
 | 
					        email: false,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(systemMock.get).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -203,7 +191,7 @@ describe(ServerInfoService.name, () => {
 | 
				
			|||||||
        isOnboarded: false,
 | 
					        isOnboarded: false,
 | 
				
			||||||
        externalDomain: '',
 | 
					        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 { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 | 
					import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
				
			||||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
 | 
					import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { asHumanReadable } from 'src/utils/bytes';
 | 
					import { asHumanReadable } from 'src/utils/bytes';
 | 
				
			||||||
@ -34,7 +33,6 @@ export class ServerInfoService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
					    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
    @Inject(IServerInfoRepository) private repository: IServerInfoRepository,
 | 
					    @Inject(IServerInfoRepository) private repository: IServerInfoRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
@ -42,7 +40,7 @@ export class ServerInfoService {
 | 
				
			|||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(ServerInfoService.name);
 | 
					    this.logger.setContext(ServerInfoService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onConnect() {}
 | 
					  onConnect() {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +1,27 @@
 | 
				
			|||||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
 | 
					 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
 | 
					import { IDatabaseRepository } from 'src/interfaces/database.interface';
 | 
				
			||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
 | 
					import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 | 
					import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 | 
				
			||||||
import { ISearchRepository } from 'src/interfaces/search.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 { SmartInfoService } from 'src/services/smart-info.service';
 | 
				
			||||||
import { getCLIPModelInfo } from 'src/utils/misc';
 | 
					import { getCLIPModelInfo } from 'src/utils/misc';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
 | 
					import { systemConfigStub } from 'test/fixtures/system-config.stub';
 | 
				
			||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
					import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
 | 
				
			||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
 | 
					import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
 | 
				
			||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
 | 
					import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
 | 
				
			||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
					import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
 | 
					import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
 | 
				
			||||||
import { newSearchRepositoryMock } from 'test/repositories/search.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';
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(SmartInfoService.name, () => {
 | 
					describe(SmartInfoService.name, () => {
 | 
				
			||||||
  let sut: SmartInfoService;
 | 
					  let sut: SmartInfoService;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let searchMock: Mocked<ISearchRepository>;
 | 
					  let searchMock: Mocked<ISearchRepository>;
 | 
				
			||||||
  let machineMock: Mocked<IMachineLearningRepository>;
 | 
					  let machineMock: Mocked<IMachineLearningRepository>;
 | 
				
			||||||
@ -30,13 +30,13 @@ describe(SmartInfoService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    searchMock = newSearchRepositoryMock();
 | 
					    searchMock = newSearchRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    machineMock = newMachineLearningRepositoryMock();
 | 
					    machineMock = newMachineLearningRepositoryMock();
 | 
				
			||||||
    databaseMock = newDatabaseRepositoryMock();
 | 
					    databaseMock = newDatabaseRepositoryMock();
 | 
				
			||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    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]);
 | 
					    assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -47,7 +47,7 @@ describe(SmartInfoService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleQueueEncodeClip', () => {
 | 
					  describe('handleQueueEncodeClip', () => {
 | 
				
			||||||
    it('should do nothing if machine learning is disabled', async () => {
 | 
					    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({});
 | 
					      await sut.handleQueueEncodeClip({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,7 +84,7 @@ describe(SmartInfoService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleEncodeClip', () => {
 | 
					  describe('handleEncodeClip', () => {
 | 
				
			||||||
    it('should do nothing if machine learning is disabled', async () => {
 | 
					    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);
 | 
					      expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ import {
 | 
				
			|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 | 
					import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 | 
				
			||||||
import { ISearchRepository } from 'src/interfaces/search.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 { isSmartSearchEnabled } from 'src/utils/misc';
 | 
				
			||||||
import { usePagination } from 'src/utils/pagination';
 | 
					import { usePagination } from 'src/utils/pagination';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,11 +28,11 @@ export class SmartInfoService {
 | 
				
			|||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
					    @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) private repository: ISearchRepository,
 | 
					    @Inject(ISearchRepository) private repository: ISearchRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(SmartInfoService.name);
 | 
					    this.logger.setContext(SmartInfoService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async init() {
 | 
					  async init() {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ import { SystemConfig, defaults } from 'src/config';
 | 
				
			|||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
 | 
					import { SystemConfigCore } from 'src/cores/system-config.core';
 | 
				
			||||||
import { AssetEntity } from 'src/entities/asset.entity';
 | 
					import { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
import { AssetPathType } from 'src/entities/move.entity';
 | 
					import { AssetPathType } from 'src/entities/move.entity';
 | 
				
			||||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
 | 
					 | 
				
			||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
					import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { ICryptoRepository } from 'src/interfaces/crypto.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 { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { StorageTemplateService } from 'src/services/storage-template.service';
 | 
					import { StorageTemplateService } from 'src/services/storage-template.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					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 { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
 | 
				
			||||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
					import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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 { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
import { Mocked } from 'vitest';
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -34,13 +33,13 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
  let sut: StorageTemplateService;
 | 
					  let sut: StorageTemplateService;
 | 
				
			||||||
  let albumMock: Mocked<IAlbumRepository>;
 | 
					  let albumMock: Mocked<IAlbumRepository>;
 | 
				
			||||||
  let assetMock: Mocked<IAssetRepository>;
 | 
					  let assetMock: Mocked<IAssetRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let cryptoMock: Mocked<ICryptoRepository>;
 | 
				
			||||||
 | 
					  let databaseMock: Mocked<IDatabaseRepository>;
 | 
				
			||||||
  let moveMock: Mocked<IMoveRepository>;
 | 
					  let moveMock: Mocked<IMoveRepository>;
 | 
				
			||||||
  let personMock: Mocked<IPersonRepository>;
 | 
					  let personMock: Mocked<IPersonRepository>;
 | 
				
			||||||
  let storageMock: Mocked<IStorageRepository>;
 | 
					  let storageMock: Mocked<IStorageRepository>;
 | 
				
			||||||
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let userMock: Mocked<IUserRepository>;
 | 
					  let userMock: Mocked<IUserRepository>;
 | 
				
			||||||
  let cryptoMock: Mocked<ICryptoRepository>;
 | 
					 | 
				
			||||||
  let databaseMock: Mocked<IDatabaseRepository>;
 | 
					 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
@ -48,23 +47,23 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					 | 
				
			||||||
    assetMock = newAssetRepositoryMock();
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    albumMock = newAlbumRepositoryMock();
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
 | 
					    cryptoMock = newCryptoRepositoryMock();
 | 
				
			||||||
 | 
					    databaseMock = newDatabaseRepositoryMock();
 | 
				
			||||||
    moveMock = newMoveRepositoryMock();
 | 
					    moveMock = newMoveRepositoryMock();
 | 
				
			||||||
    personMock = newPersonRepositoryMock();
 | 
					    personMock = newPersonRepositoryMock();
 | 
				
			||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					 | 
				
			||||||
    databaseMock = newDatabaseRepositoryMock();
 | 
					 | 
				
			||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    loggerMock = newLoggerRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    configMock.load.mockResolvedValue([{ key: SystemConfigKey.STORAGE_TEMPLATE_ENABLED, value: true }]);
 | 
					    systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new StorageTemplateService(
 | 
					    sut = new StorageTemplateService(
 | 
				
			||||||
      albumMock,
 | 
					      albumMock,
 | 
				
			||||||
      assetMock,
 | 
					      assetMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      moveMock,
 | 
					      moveMock,
 | 
				
			||||||
      personMock,
 | 
					      personMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
@ -74,7 +73,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
      loggerMock,
 | 
					      loggerMock,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    SystemConfigCore.create(configMock, loggerMock).config$.next(defaults);
 | 
					    SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('onValidateConfig', () => {
 | 
					  describe('onValidateConfig', () => {
 | 
				
			||||||
@ -108,7 +107,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('handleMigrationSingle', () => {
 | 
					  describe('handleMigrationSingle', () => {
 | 
				
			||||||
    it('should skip when storage template is disabled', async () => {
 | 
					    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);
 | 
					      await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
 | 
				
			||||||
      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
					      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(storageMock.checkFileExists).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 { IMoveRepository } from 'src/interfaces/move.interface';
 | 
				
			||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
					import { IPersonRepository } from 'src/interfaces/person.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { getLivePhotoMotionFilename } from 'src/utils/file';
 | 
					import { getLivePhotoMotionFilename } from 'src/utils/file';
 | 
				
			||||||
import { usePagination } from 'src/utils/pagination';
 | 
					import { usePagination } from 'src/utils/pagination';
 | 
				
			||||||
@ -65,7 +65,7 @@ export class StorageTemplateService {
 | 
				
			|||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
					    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
					    @Inject(IMoveRepository) moveRepository: IMoveRepository,
 | 
				
			||||||
    @Inject(IPersonRepository) personRepository: IPersonRepository,
 | 
					    @Inject(IPersonRepository) personRepository: IPersonRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
@ -75,7 +75,7 @@ export class StorageTemplateService {
 | 
				
			|||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.logger.setContext(StorageTemplateService.name);
 | 
					    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.configCore.config$.subscribe((config) => this.onConfig(config));
 | 
				
			||||||
    this.storageCore = StorageCore.create(
 | 
					    this.storageCore = StorageCore.create(
 | 
				
			||||||
      assetRepository,
 | 
					      assetRepository,
 | 
				
			||||||
@ -83,7 +83,7 @@ export class StorageTemplateService {
 | 
				
			|||||||
      moveRepository,
 | 
					      moveRepository,
 | 
				
			||||||
      personRepository,
 | 
					      personRepository,
 | 
				
			||||||
      storageRepository,
 | 
					      storageRepository,
 | 
				
			||||||
      configRepository,
 | 
					      systemMetadataRepository,
 | 
				
			||||||
      this.logger,
 | 
					      this.logger,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -12,24 +12,25 @@ import {
 | 
				
			|||||||
  VideoCodec,
 | 
					  VideoCodec,
 | 
				
			||||||
  defaults,
 | 
					  defaults,
 | 
				
			||||||
} from 'src/config';
 | 
					} 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 { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
 | 
				
			||||||
import { QueueName } from 'src/interfaces/job.interface';
 | 
					import { QueueName } from 'src/interfaces/job.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { ISearchRepository } from 'src/interfaces/search.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 { SystemConfigService } from 'src/services/system-config.service';
 | 
				
			||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
 | 
					import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
 | 
				
			||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.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';
 | 
					import { Mocked } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updates: SystemConfigEntity[] = [
 | 
					const partialConfig = {
 | 
				
			||||||
  { key: SystemConfigKey.FFMPEG_CRF, value: 30 },
 | 
					  ffmpeg: { crf: 30 },
 | 
				
			||||||
  { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true },
 | 
					  oauth: { autoLaunch: true },
 | 
				
			||||||
  { key: SystemConfigKey.TRASH_DAYS, value: 10 },
 | 
					  trash: { days: 10 },
 | 
				
			||||||
  { key: SystemConfigKey.USER_DELETE_DELAY, value: 15 },
 | 
					  user: { deleteDelay: 15 },
 | 
				
			||||||
];
 | 
					} satisfies DeepPartial<SystemConfig>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updatedConfig = Object.freeze<SystemConfig>({
 | 
					const updatedConfig = Object.freeze<SystemConfig>({
 | 
				
			||||||
  job: {
 | 
					  job: {
 | 
				
			||||||
@ -171,17 +172,17 @@ const updatedConfig = Object.freeze<SystemConfig>({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
describe(SystemConfigService.name, () => {
 | 
					describe(SystemConfigService.name, () => {
 | 
				
			||||||
  let sut: SystemConfigService;
 | 
					  let sut: SystemConfigService;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let eventMock: Mocked<IEventRepository>;
 | 
					  let eventMock: Mocked<IEventRepository>;
 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
  let smartInfoMock: Mocked<ISearchRepository>;
 | 
					  let smartInfoMock: Mocked<ISearchRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    delete process.env.IMMICH_CONFIG_FILE;
 | 
					    delete process.env.IMMICH_CONFIG_FILE;
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    eventMock = newEventRepositoryMock();
 | 
					    eventMock = newEventRepositoryMock();
 | 
				
			||||||
    loggerMock = newLoggerRepositoryMock();
 | 
					    loggerMock = newLoggerRepositoryMock();
 | 
				
			||||||
    sut = new SystemConfigService(configMock, eventMock, loggerMock, smartInfoMock);
 | 
					    sut = new SystemConfigService(systemMock, eventMock, loggerMock, smartInfoMock);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
@ -190,44 +191,39 @@ describe(SystemConfigService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('getDefaults', () => {
 | 
					  describe('getDefaults', () => {
 | 
				
			||||||
    it('should return the default config', () => {
 | 
					    it('should return the default config', () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(updates);
 | 
					      systemMock.get.mockResolvedValue(partialConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(sut.getDefaults()).toEqual(defaults);
 | 
					      expect(sut.getDefaults()).toEqual(defaults);
 | 
				
			||||||
      expect(configMock.load).not.toHaveBeenCalled();
 | 
					      expect(systemMock.get).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('getConfig', () => {
 | 
					  describe('getConfig', () => {
 | 
				
			||||||
    it('should return the default config', async () => {
 | 
					    it('should return the default config', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue([]);
 | 
					      systemMock.get.mockResolvedValue({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.getConfig()).resolves.toEqual(defaults);
 | 
					      await expect(sut.getConfig()).resolves.toEqual(defaults);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should merge the overrides', async () => {
 | 
					    it('should merge the overrides', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue([
 | 
					      systemMock.get.mockResolvedValue({
 | 
				
			||||||
        { key: SystemConfigKey.FFMPEG_CRF, value: 30 },
 | 
					        ffmpeg: { crf: 30 },
 | 
				
			||||||
        { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true },
 | 
					        oauth: { autoLaunch: true },
 | 
				
			||||||
        { key: SystemConfigKey.TRASH_DAYS, value: 10 },
 | 
					        trash: { days: 10 },
 | 
				
			||||||
        { key: SystemConfigKey.USER_DELETE_DELAY, value: 15 },
 | 
					        user: { deleteDelay: 15 },
 | 
				
			||||||
      ]);
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
 | 
					      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should load the config from a json file', async () => {
 | 
					    it('should load the config from a json file', async () => {
 | 
				
			||||||
      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
					      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
				
			||||||
      const partialConfig = {
 | 
					
 | 
				
			||||||
        ffmpeg: { crf: 30 },
 | 
					      systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
 | 
				
			||||||
        oauth: { autoLaunch: true },
 | 
					 | 
				
			||||||
        trash: { days: 10 },
 | 
					 | 
				
			||||||
        user: { deleteDelay: 15 },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
      configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
 | 
					      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 () => {
 | 
					    it('should load the config from a yaml file', async () => {
 | 
				
			||||||
@ -242,26 +238,26 @@ describe(SystemConfigService.name, () => {
 | 
				
			|||||||
        user:
 | 
					        user:
 | 
				
			||||||
          deleteDelay: 15
 | 
					          deleteDelay: 15
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      configMock.readFile.mockResolvedValue(partialConfig);
 | 
					      systemMock.readFile.mockResolvedValue(partialConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
 | 
					      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 () => {
 | 
					    it('should accept an empty configuration file', async () => {
 | 
				
			||||||
      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
					      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);
 | 
					      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 () => {
 | 
					    it('should allow underscores in the machine learning url', async () => {
 | 
				
			||||||
      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
					      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
				
			||||||
      const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
 | 
					      const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
 | 
				
			||||||
      configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
 | 
					      systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const config = await sut.getConfig();
 | 
					      const config = await sut.getConfig();
 | 
				
			||||||
      expect(config.machineLearning.url).toEqual('immich_machine_learning');
 | 
					      expect(config.machineLearning.url).toEqual('immich_machine_learning');
 | 
				
			||||||
@ -272,7 +268,7 @@ describe(SystemConfigService.name, () => {
 | 
				
			|||||||
      const partialConfig = `
 | 
					      const partialConfig = `
 | 
				
			||||||
        unknownOption: true
 | 
					        unknownOption: true
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      configMock.readFile.mockResolvedValue(partialConfig);
 | 
					      systemMock.readFile.mockResolvedValue(partialConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.getConfig();
 | 
					      await sut.getConfig();
 | 
				
			||||||
      expect(loggerMock.warn).toHaveBeenCalled();
 | 
					      expect(loggerMock.warn).toHaveBeenCalled();
 | 
				
			||||||
@ -290,7 +286,7 @@ describe(SystemConfigService.name, () => {
 | 
				
			|||||||
    for (const test of tests) {
 | 
					    for (const test of tests) {
 | 
				
			||||||
      it(`should ${test.should}`, async () => {
 | 
					      it(`should ${test.should}`, async () => {
 | 
				
			||||||
        process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
					        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) {
 | 
					        if (test.warn) {
 | 
				
			||||||
          await sut.getConfig();
 | 
					          await sut.getConfig();
 | 
				
			||||||
@ -338,20 +334,20 @@ describe(SystemConfigService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  describe('updateConfig', () => {
 | 
					  describe('updateConfig', () => {
 | 
				
			||||||
    it('should update the config and emit client and server events', async () => {
 | 
					    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);
 | 
					      await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(eventMock.clientBroadcast).toHaveBeenCalled();
 | 
					      expect(eventMock.clientBroadcast).toHaveBeenCalled();
 | 
				
			||||||
      expect(eventMock.serverSend).toHaveBeenCalledWith(ServerEvent.CONFIG_UPDATE, null);
 | 
					      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 () => {
 | 
					    it('should throw an error if a config file is in use', async () => {
 | 
				
			||||||
      process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
 | 
					      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);
 | 
					      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';
 | 
					} from 'src/interfaces/event.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { ISearchRepository } from 'src/interfaces/search.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()
 | 
					@Injectable()
 | 
				
			||||||
export class SystemConfigService {
 | 
					export class SystemConfigService {
 | 
				
			||||||
  private core: SystemConfigCore;
 | 
					  private core: SystemConfigCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(ISystemConfigRepository) private repository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
					    @Inject(IEventRepository) private eventRepository: IEventRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
    @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
 | 
					    @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 { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { UserService } from 'src/services/user.service';
 | 
					import { UserService } from 'src/services/user.service';
 | 
				
			||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
					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 { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
 | 
				
			||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
					import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
 | 
				
			||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.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 { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
 | 
				
			||||||
import { Mocked, vitest } from 'vitest';
 | 
					import { Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,12 +44,12 @@ describe(UserService.name, () => {
 | 
				
			|||||||
  let jobMock: Mocked<IJobRepository>;
 | 
					  let jobMock: Mocked<IJobRepository>;
 | 
				
			||||||
  let libraryMock: Mocked<ILibraryRepository>;
 | 
					  let libraryMock: Mocked<ILibraryRepository>;
 | 
				
			||||||
  let storageMock: Mocked<IStorageRepository>;
 | 
					  let storageMock: Mocked<IStorageRepository>;
 | 
				
			||||||
  let configMock: Mocked<ISystemConfigRepository>;
 | 
					  let systemMock: Mocked<ISystemMetadataRepository>;
 | 
				
			||||||
  let loggerMock: Mocked<ILoggerRepository>;
 | 
					  let loggerMock: Mocked<ILoggerRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    albumMock = newAlbumRepositoryMock();
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
    configMock = newSystemConfigRepositoryMock();
 | 
					    systemMock = newSystemMetadataRepositoryMock();
 | 
				
			||||||
    cryptoRepositoryMock = newCryptoRepositoryMock();
 | 
					    cryptoRepositoryMock = newCryptoRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    libraryMock = newLibraryRepositoryMock();
 | 
					    libraryMock = newLibraryRepositoryMock();
 | 
				
			||||||
@ -63,7 +63,7 @@ describe(UserService.name, () => {
 | 
				
			|||||||
      jobMock,
 | 
					      jobMock,
 | 
				
			||||||
      libraryMock,
 | 
					      libraryMock,
 | 
				
			||||||
      storageMock,
 | 
					      storageMock,
 | 
				
			||||||
      configMock,
 | 
					      systemMock,
 | 
				
			||||||
      userMock,
 | 
					      userMock,
 | 
				
			||||||
      loggerMock,
 | 
					      loggerMock,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -486,7 +486,7 @@ describe(UserService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip users not ready for deletion - deleteDelay30', async () => {
 | 
					    it('should skip users not ready for deletion - deleteDelay30', async () => {
 | 
				
			||||||
      configMock.load.mockResolvedValue(systemConfigStub.deleteDelay30);
 | 
					      systemMock.get.mockResolvedValue(systemConfigStub.deleteDelay30);
 | 
				
			||||||
      userMock.getDeletedUsers.mockResolvedValue([
 | 
					      userMock.getDeletedUsers.mockResolvedValue([
 | 
				
			||||||
        {},
 | 
					        {},
 | 
				
			||||||
        { deletedAt: undefined },
 | 
					        { deletedAt: undefined },
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/j
 | 
				
			|||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
					import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
				
			||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
					import { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.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 { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
 | 
				
			||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
					import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,13 +28,13 @@ export class UserService {
 | 
				
			|||||||
    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
    @Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
 | 
					    @Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
 | 
				
			||||||
    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
					    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
 | 
				
			||||||
    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
 | 
					    @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
 | 
				
			||||||
    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
					    @Inject(IUserRepository) private userRepository: IUserRepository,
 | 
				
			||||||
    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
					    @Inject(ILoggerRepository) private logger: ILoggerRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
 | 
					    this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
 | 
				
			||||||
    this.logger.setContext(UserService.name);
 | 
					    this.logger.setContext(UserService.name);
 | 
				
			||||||
    this.configCore = SystemConfigCore.create(configRepository, this.logger);
 | 
					    this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async listUsers(): Promise<UserResponseDto[]> {
 | 
					  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 { ILoggerRepository } from 'src/interfaces/logger.interface';
 | 
				
			||||||
import { Metadata } from 'src/middleware/auth.guard';
 | 
					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;
 | 
					const isMachineLearningEnabled = (machineLearning: SystemConfig['machineLearning']) => machineLearning.enabled;
 | 
				
			||||||
export const isSmartSearchEnabled = (machineLearning: SystemConfig['machineLearning']) =>
 | 
					export const isSmartSearchEnabled = (machineLearning: SystemConfig['machineLearning']) =>
 | 
				
			||||||
  isMachineLearningEnabled(machineLearning) && machineLearning.clip.enabled;
 | 
					  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[]> = {
 | 
					export const systemConfigStub = {
 | 
				
			||||||
  defaults: [],
 | 
					  enabled: {
 | 
				
			||||||
  enabled: [
 | 
					    oauth: {
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_ENABLED, value: true },
 | 
					      enabled: true,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
 | 
					      autoRegister: true,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: false },
 | 
					      autoLaunch: false,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
 | 
					      buttonText: 'OAuth',
 | 
				
			||||||
  ],
 | 
					    },
 | 
				
			||||||
  disabled: [{ key: SystemConfigKey.PASSWORD_LOGIN_ENABLED, value: false }],
 | 
					  },
 | 
				
			||||||
  noAutoRegister: [
 | 
					  disabled: {
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_ENABLED, value: true },
 | 
					    passwordLogin: {
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: false },
 | 
					      enabled: false,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: false },
 | 
					    },
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
 | 
					  },
 | 
				
			||||||
  ],
 | 
					  noAutoRegister: {
 | 
				
			||||||
  override: [
 | 
					    oauth: {
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_ENABLED, value: true },
 | 
					      enabled: true,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
 | 
					      autoRegister: false,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_MOBILE_OVERRIDE_ENABLED, value: true },
 | 
					      autoLaunch: false,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_MOBILE_REDIRECT_URI, value: 'http://mobile-redirect' },
 | 
					      buttonText: 'OAuth',
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
 | 
					    },
 | 
				
			||||||
  ],
 | 
					  },
 | 
				
			||||||
  withDefaultStorageQuota: [
 | 
					  override: {
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_ENABLED, value: true },
 | 
					    oauth: {
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
 | 
					      enabled: true,
 | 
				
			||||||
    { key: SystemConfigKey.OAUTH_DEFAULT_STORAGE_QUOTA, value: 1 },
 | 
					      autoRegister: true,
 | 
				
			||||||
  ],
 | 
					      mobileOverrideEnabled: true,
 | 
				
			||||||
  deleteDelay30: [{ key: SystemConfigKey.USER_DELETE_DELAY, value: 30 }],
 | 
					      mobileRedirectUri: 'http://mobile-redirect',
 | 
				
			||||||
  libraryWatchEnabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: true }],
 | 
					      buttonText: 'OAuth',
 | 
				
			||||||
  libraryWatchDisabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: false }],
 | 
					    },
 | 
				
			||||||
};
 | 
					  },
 | 
				
			||||||
 | 
					  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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 | 
				
			||||||
import { Mocked, vitest } from 'vitest';
 | 
					import { Mocked, vitest } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const newSystemMetadataRepositoryMock = (): Mocked<ISystemMetadataRepository> => {
 | 
					export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMetadataRepository> => {
 | 
				
			||||||
 | 
					  if (reset) {
 | 
				
			||||||
 | 
					    SystemConfigCore.reset();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    get: vitest.fn() as any,
 | 
					    get: vitest.fn() as any,
 | 
				
			||||||
    set: vitest.fn(),
 | 
					    set: vitest.fn(),
 | 
				
			||||||
 | 
					    readFile: vitest.fn(),
 | 
				
			||||||
 | 
					    fetchStyle: vitest.fn(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user