mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
refactor(server): system config (#9517)
This commit is contained in:
parent
7f0f016f2e
commit
984aa8fb41
46 changed files with 599 additions and 770 deletions
|
@ -96,7 +96,7 @@ SELECT * FROM "users";
|
||||||
## System Config
|
## 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…
Reference in a new issue