diff --git a/ansible/roles/media-metadata-sync/defaults/main.yml b/ansible/roles/media-metadata-sync/defaults/main.yml new file mode 100644 index 00000000000..fd5db6d673f --- /dev/null +++ b/ansible/roles/media-metadata-sync/defaults/main.yml @@ -0,0 +1 @@ +SERVER_MEDIA_METADATA_SYNC_CRONJOB_SCHEDULE: "30 2 * * *" diff --git a/ansible/roles/media-metadata-sync/meta/main.yml b/ansible/roles/media-metadata-sync/meta/main.yml new file mode 100644 index 00000000000..4f0fdb787f4 --- /dev/null +++ b/ansible/roles/media-metadata-sync/meta/main.yml @@ -0,0 +1,9 @@ +galaxy_info: + role_name: media-metadata-sync + author: Schul-Cloud Verbund + description: media-metadata-sync role for the media metadata synchronization purposes + company: Schul-Cloud Verbund + license: license (AGPLv3) + min_ansible_version: 2.8 + galaxy_tags: [] +dependencies: [] diff --git a/ansible/roles/media-metadata-sync/tasks/main.yml b/ansible/roles/media-metadata-sync/tasks/main.yml new file mode 100644 index 00000000000..c2c9a0f1295 --- /dev/null +++ b/ansible/roles/media-metadata-sync/tasks/main.yml @@ -0,0 +1,17 @@ +- name: media metadata sync CronJob ConfigMap + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: media-metadata-sync-cronjob-configmap.yml.j2 + state: "{{ 'present' if WITH_MEDIA_METADATA_SYNC is defined and WITH_MEDIA_METADATA_SYNC|bool else 'absent'}}" + tags: + - configmap + +- name: media metadata sync sync CronJob + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: media-metadata-sync-cronjob.yml.j2 + state: "{{ 'present' if WITH_MEDIA_METADATA_SYNC is defined and WITH_MEDIA_METADATA_SYNC|bool else 'absent'}}" + tags: + - cronjob diff --git a/ansible/roles/media-metadata-sync/templates/media-metadata-sync-cronjob-configmap.yml.j2 b/ansible/roles/media-metadata-sync/templates/media-metadata-sync-cronjob-configmap.yml.j2 new file mode 100644 index 00000000000..6b0a8242ec7 --- /dev/null +++ b/ansible/roles/media-metadata-sync/templates/media-metadata-sync-cronjob-configmap.yml.j2 @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: {{ NAMESPACE }} + name: media-metadata-sync-cronjob-configmap + labels: + app: media-metadata-sync-cronjob +data: + NODE_OPTIONS: "--max-old-space-size=1536" + NEST_LOG_LEVEL: "info" + EXIT_ON_ERROR: "true" diff --git a/ansible/roles/media-metadata-sync/templates/media-metadata-sync-cronjob.yml.j2 b/ansible/roles/media-metadata-sync/templates/media-metadata-sync-cronjob.yml.j2 new file mode 100644 index 00000000000..ec55dbb3a6a --- /dev/null +++ b/ansible/roles/media-metadata-sync/templates/media-metadata-sync-cronjob.yml.j2 @@ -0,0 +1,97 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + namespace: {{ NAMESPACE }} + labels: + app: media-metadata-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: media-metadata-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} + name: media-metadata-sync-cronjob +spec: + schedule: {{ SERVER_MEDIA_METADATA_SYNC_CRONJOB_SCHEDULE|default("30 2 * * *", true) }} + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + metadata: + labels: + app: media-metadata-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: media-metadata-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} + spec: + containers: + - name: media-metadata-sync-cronjob + image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} + envFrom: + - configMapRef: + name: api-configmap + - secretRef: + name: api-secret + - configMapRef: + name: media-metadata-sync-cronjob-configmap + command: ['/bin/sh','-c'] + args: ['npm run nest:start:sync:media-metadata'] + resources: + limits: + cpu: {{ MEDIA_METADATA_SYNC_CPU_LIMITS|default("2000m", true) }} + memory: {{ MEDIA_METADATA_SYNC_MEMORY_LIMITS|default("2Gi", true) }} + requests: + cpu: {{ MEDIA_METADATA_SYNC_CPU_REQUESTS|default("100m", true) }} + memory: {{ MEDIA_METADATA_SYNC_MEMORY_REQUESTS|default("150Mi", true) }} + restartPolicy: OnFailure +{% if AFFINITY_ENABLE is defined and AFFINITY_ENABLE|bool %} + affinity: + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 20 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/part-of + operator: In + values: + - schulcloud-verbund + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} + - weight: 10 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: git.repo + operator: In + values: + - {{ SCHULCLOUD_SERVER_REPO_NAME }} + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} + - weight: 10 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: git.branch + operator: In + values: + - {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} + - weight: 10 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/version + operator: In + values: + - {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} +{% endif %} + diff --git a/apps/server/src/infra/sync/console/README.md b/apps/server/src/infra/sync/console/README.md index 130dc31988b..8141b6b0610 100644 --- a/apps/server/src/infra/sync/console/README.md +++ b/apps/server/src/infra/sync/console/README.md @@ -10,13 +10,14 @@ npm run nest:start:console sync run Where `` is the name of the system you want to start the synchronization for. The currently available systems are: - `tsp` - Synchronize Thüringer schulportal. - `vidis` - Synchronize Vidis Activation Data. +- `media_metadata` - Synchronize media metadata in ctl tools in SVS with data from its media source. If the target is not provided, the synchronization will not start and the available targets will be displayed in an error message. ```bash { message: 'Either synchronization is not activated or the target entered is invalid', - data: { enteredTarget: 'tsp', availableTargets: { TSP: 'tsp', VIDIS: 'vidis' }} + data: { enteredTarget: 'tsp', availableTargets: { TSP: 'tsp', VIDIS: 'vidis', MEDIA_METADTA: 'media_metadata' }} } ``` diff --git a/apps/server/src/infra/sync/index.ts b/apps/server/src/infra/sync/index.ts index 02a7612b301..8cd53e619fb 100644 --- a/apps/server/src/infra/sync/index.ts +++ b/apps/server/src/infra/sync/index.ts @@ -1,2 +1,3 @@ export * from './tsp'; export * from './media-licenses'; +export * from './media-metadata'; diff --git a/apps/server/src/infra/sync/media-metadata/index.ts b/apps/server/src/infra/sync/media-metadata/index.ts new file mode 100644 index 00000000000..9f73814a83e --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/index.ts @@ -0,0 +1 @@ +export { MediaMetadataSyncStrategy } from './strategy'; diff --git a/apps/server/src/infra/sync/media-metadata/loggable/index.ts b/apps/server/src/infra/sync/media-metadata/loggable/index.ts new file mode 100644 index 00000000000..461925fc0d9 --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/loggable/index.ts @@ -0,0 +1 @@ +export { MediaMetadataSyncReportLoggable } from './media-metadata-sync-report.loggable'; diff --git a/apps/server/src/infra/sync/media-metadata/loggable/media-metadata-sync-report.loggable.spec.ts b/apps/server/src/infra/sync/media-metadata/loggable/media-metadata-sync-report.loggable.spec.ts new file mode 100644 index 00000000000..c44cb96fd37 --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/loggable/media-metadata-sync-report.loggable.spec.ts @@ -0,0 +1,48 @@ +import { MediaSourceSyncOperationReport } from '@modules/media-source/domain'; +import { mediaSourceSyncReportFactory } from '@modules/media-source/testing'; +import { MediaMetadataSyncReportLoggable } from './media-metadata-sync-report.loggable'; + +describe(MediaMetadataSyncReportLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const report = mediaSourceSyncReportFactory.build(); + + const loggable = new MediaMetadataSyncReportLoggable(report); + + let expectedMessage = + 'Media metadata sync had finished\n' + + `Total media processed: ${report.totalCount}\n` + + `Total successful sync: ${report.successCount}\n` + + `Total failed sync: ${report.failedCount}\n` + + `Total undelivered media: ${report.undeliveredCount}\n`; + + const operationsString = report.operations + .map( + (operation: MediaSourceSyncOperationReport): string => + `${operation.operation} operation, Status: ${operation.status}, Total: ${operation.count}` + ) + .join('\n'); + + expectedMessage += operationsString; + + return { + loggable, + report, + expectedMessage, + }; + }; + + it('should return the correct log message', () => { + const { loggable, report, expectedMessage } = setup(); + + const logMessage = loggable.getLogMessage(); + + expect(logMessage).toEqual({ + message: expectedMessage, + data: { + report: JSON.stringify(report), + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/media-metadata/loggable/media-metadata-sync-report.loggable.ts b/apps/server/src/infra/sync/media-metadata/loggable/media-metadata-sync-report.loggable.ts new file mode 100644 index 00000000000..53d0ff0ec84 --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/loggable/media-metadata-sync-report.loggable.ts @@ -0,0 +1,40 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@core/logger'; +import { MediaSourceSyncOperationReport, MediaSourceSyncReport } from '@modules/media-source/domain'; + +export class MediaMetadataSyncReportLoggable implements Loggable { + constructor(private readonly report: MediaSourceSyncReport) {} + + public getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + const message = `Media metadata sync had finished\n${this.formatCountOverview(this.report)}${this.formatOperations( + this.report.operations + )}`; + + return { + message, + data: { + report: JSON.stringify(this.report), + }, + }; + } + + private formatCountOverview(syncReport: MediaSourceSyncReport): string { + const formattedString = + `Total media processed: ${syncReport.totalCount}\n` + + `Total successful sync: ${syncReport.successCount}\n` + + `Total failed sync: ${syncReport.failedCount}\n` + + `Total undelivered media: ${syncReport.undeliveredCount}\n`; + + return formattedString; + } + + private formatOperations(operations: MediaSourceSyncOperationReport[]): string { + const formattedString = operations + .map( + (operation: MediaSourceSyncOperationReport): string => + `${operation.operation} operation, Status: ${operation.status}, Total: ${operation.count}` + ) + .join('\n'); + + return formattedString; + } +} diff --git a/apps/server/src/infra/sync/media-metadata/strategy/index.ts b/apps/server/src/infra/sync/media-metadata/strategy/index.ts new file mode 100644 index 00000000000..d4b744cd914 --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/strategy/index.ts @@ -0,0 +1 @@ +export { MediaMetadataSyncStrategy } from './media-metadata-sync.strategy'; diff --git a/apps/server/src/infra/sync/media-metadata/strategy/media-metadata-sync.strategy.spec.ts b/apps/server/src/infra/sync/media-metadata/strategy/media-metadata-sync.strategy.spec.ts new file mode 100644 index 00000000000..1092b5bace2 --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/strategy/media-metadata-sync.strategy.spec.ts @@ -0,0 +1,74 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Logger } from '@core/logger'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { MediaSourceSyncService } from '@modules/media-source/service'; +import { mediaSourceSyncReportFactory } from '@modules/media-source/testing'; +import { SyncStrategyTarget } from '../../sync-strategy.types'; +import { MediaMetadataSyncReportLoggable } from '../loggable'; +import { MediaMetadataSyncStrategy } from './media-metadata-sync.strategy'; + +describe(MediaMetadataSyncStrategy.name, () => { + let module: TestingModule; + let strategy: MediaMetadataSyncStrategy; + let syncService: DeepMocked; + let logger: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + MediaMetadataSyncStrategy, + { + provide: MediaSourceSyncService, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, + ], + }).compile(); + + strategy = module.get(MediaMetadataSyncStrategy); + syncService = module.get(MediaSourceSyncService); + logger = module.get(Logger); + }); + + describe('getType', () => { + describe('when the method is called', () => { + it('should return the correct sync target', () => { + const result = strategy.getType(); + + expect(result).toEqual(SyncStrategyTarget.MEDIA_METADATA); + }); + }); + }); + + describe('sync', () => { + describe('when the method is called', () => { + const setup = () => { + const report = mediaSourceSyncReportFactory.build(); + + syncService.syncAllMediaMetadata.mockResolvedValue(report); + + return { report }; + }; + + it('should start the sync for media metadata', async () => { + setup(); + + await strategy.sync(); + + expect(syncService.syncAllMediaMetadata).toBeCalled(); + }); + + it('should log the report after sync', async () => { + const { report } = setup(); + + await strategy.sync(); + + const loggable = new MediaMetadataSyncReportLoggable(report); + expect(logger.info).toBeCalledWith(loggable); + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/media-metadata/strategy/media-metadata-sync.strategy.ts b/apps/server/src/infra/sync/media-metadata/strategy/media-metadata-sync.strategy.ts new file mode 100644 index 00000000000..eff42fd15a5 --- /dev/null +++ b/apps/server/src/infra/sync/media-metadata/strategy/media-metadata-sync.strategy.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { Logger } from '@core/logger'; +import { MediaSourceSyncService } from '@modules/media-source/service'; +import { MediaSourceSyncReport } from '@modules/media-source/domain'; +import { SyncStrategy } from '../../strategy/sync-strategy'; +import { SyncStrategyTarget } from '../../sync-strategy.types'; +import { MediaMetadataSyncReportLoggable } from '../loggable'; + +@Injectable() +export class MediaMetadataSyncStrategy implements SyncStrategy { + constructor(private readonly mediaSourceSyncService: MediaSourceSyncService, private readonly logger: Logger) {} + + public getType(): SyncStrategyTarget { + return SyncStrategyTarget.MEDIA_METADATA; + } + + public async sync(): Promise { + const report: MediaSourceSyncReport = await this.mediaSourceSyncService.syncAllMediaMetadata(); + + this.logSyncReport(report); + } + + private logSyncReport(report: MediaSourceSyncReport): void { + const loggable = new MediaMetadataSyncReportLoggable(report); + + this.logger.info(loggable); + } +} diff --git a/apps/server/src/infra/sync/service/sync.service.ts b/apps/server/src/infra/sync/service/sync.service.ts index 44124cb37c5..dbfb34270c9 100644 --- a/apps/server/src/infra/sync/service/sync.service.ts +++ b/apps/server/src/infra/sync/service/sync.service.ts @@ -5,6 +5,7 @@ import { SyncStrategy } from '../strategy/sync-strategy'; import { SyncStrategyTarget } from '../sync-strategy.types'; import { InvalidTargetLoggable } from '../errors/invalid-target.loggable'; import { VidisSyncStrategy } from '../media-licenses/strategy'; +import { MediaMetadataSyncStrategy } from '../media-metadata/strategy'; @Injectable() export class SyncService { @@ -13,11 +14,12 @@ export class SyncService { constructor( private readonly logger: Logger, @Optional() private readonly tspSyncStrategy?: TspSyncStrategy, - @Optional() private readonly vidisSyncStrategy?: VidisSyncStrategy + @Optional() private readonly vidisSyncStrategy?: VidisSyncStrategy, + @Optional() private readonly mediaMetadataSyncStrategy?: MediaMetadataSyncStrategy ) { this.logger.setContext(SyncService.name); this.registerStrategy(tspSyncStrategy); - this.registerStrategy(vidisSyncStrategy); + this.registerStrategy(mediaMetadataSyncStrategy); } protected registerStrategy(strategy?: SyncStrategy) { diff --git a/apps/server/src/infra/sync/sync-strategy.types.ts b/apps/server/src/infra/sync/sync-strategy.types.ts index 0805f03730d..3f701750bd6 100644 --- a/apps/server/src/infra/sync/sync-strategy.types.ts +++ b/apps/server/src/infra/sync/sync-strategy.types.ts @@ -1,4 +1,5 @@ export enum SyncStrategyTarget { TSP = 'tsp', VIDIS = 'vidis', + MEDIA_METADATA = 'media-metadata', } diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index c65bb4201a5..20184bfe74b 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -16,6 +16,7 @@ import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { SyncConsole } from './console/sync.console'; import { VidisFetchService, VidisSyncService, VidisSyncStrategy } from './media-licenses'; +import { MediaMetadataSyncStrategy } from './media-metadata'; import { SyncService } from './service/sync.service'; import { TspFetchService } from './tsp/tsp-fetch.service'; import { TspLegacyMigrationService } from './tsp/tsp-legacy-migration.service'; @@ -65,6 +66,7 @@ import { SyncUc } from './uc/sync.uc'; VidisSyncService, VidisSyncStrategy, VidisFetchService, + MediaMetadataSyncStrategy, ], exports: [SyncConsole], }) diff --git a/apps/server/src/modules/media-source/domain/do/index.ts b/apps/server/src/modules/media-source/domain/do/index.ts new file mode 100644 index 00000000000..d007004c6d7 --- /dev/null +++ b/apps/server/src/modules/media-source/domain/do/index.ts @@ -0,0 +1,3 @@ +export { MediaSource, MediaSourceProps } from './media-source'; +export { MediaSourceBasicAuthConfig } from './media-source-basic-auth-config'; +export { MediaSourceOauthConfig } from './media-source-oauth-config'; diff --git a/apps/server/src/modules/media-source/domain/media-source-basic-auth-config.ts b/apps/server/src/modules/media-source/domain/do/media-source-basic-auth-config.ts similarity index 100% rename from apps/server/src/modules/media-source/domain/media-source-basic-auth-config.ts rename to apps/server/src/modules/media-source/domain/do/media-source-basic-auth-config.ts diff --git a/apps/server/src/modules/media-source/domain/media-source-oauth-config.ts b/apps/server/src/modules/media-source/domain/do/media-source-oauth-config.ts similarity index 87% rename from apps/server/src/modules/media-source/domain/media-source-oauth-config.ts rename to apps/server/src/modules/media-source/domain/do/media-source-oauth-config.ts index 38fccb84b31..f9668783f91 100644 --- a/apps/server/src/modules/media-source/domain/media-source-oauth-config.ts +++ b/apps/server/src/modules/media-source/domain/do/media-source-oauth-config.ts @@ -1,4 +1,4 @@ -import { MediaSourceAuthMethod } from '../enum'; +import { MediaSourceAuthMethod } from '../../enum'; export class MediaSourceOauthConfig { public clientId: string; diff --git a/apps/server/src/modules/media-source/domain/media-source.ts b/apps/server/src/modules/media-source/domain/do/media-source.ts similarity index 94% rename from apps/server/src/modules/media-source/domain/media-source.ts rename to apps/server/src/modules/media-source/domain/do/media-source.ts index 6624b4c56ba..e793a2018ca 100644 --- a/apps/server/src/modules/media-source/domain/media-source.ts +++ b/apps/server/src/modules/media-source/domain/do/media-source.ts @@ -1,7 +1,7 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { MediaSourceBasicAuthConfig } from './media-source-basic-auth-config'; import { MediaSourceOauthConfig } from './media-source-oauth-config'; -import { MediaSourceDataFormat } from '../enum'; +import { MediaSourceDataFormat } from '../../enum'; export interface MediaSourceProps extends AuthorizableObject { id: string; diff --git a/apps/server/src/modules/media-source/domain/index.ts b/apps/server/src/modules/media-source/domain/index.ts index d007004c6d7..1b57e07b2ca 100644 --- a/apps/server/src/modules/media-source/domain/index.ts +++ b/apps/server/src/modules/media-source/domain/index.ts @@ -1,3 +1,2 @@ -export { MediaSource, MediaSourceProps } from './media-source'; -export { MediaSourceBasicAuthConfig } from './media-source-basic-auth-config'; -export { MediaSourceOauthConfig } from './media-source-oauth-config'; +export * from './do'; +export * from './interface'; diff --git a/apps/server/src/modules/media-source/domain/interface/index.ts b/apps/server/src/modules/media-source/domain/interface/index.ts new file mode 100644 index 00000000000..019d4edf2ac --- /dev/null +++ b/apps/server/src/modules/media-source/domain/interface/index.ts @@ -0,0 +1,2 @@ +export { MediaSourceSyncReport } from './media-source-sync-report'; +export { MediaSourceSyncOperationReport } from './media-source-sync-operation-report'; diff --git a/apps/server/src/modules/media-source/domain/interface/media-source-sync-operation-report.ts b/apps/server/src/modules/media-source/domain/interface/media-source-sync-operation-report.ts new file mode 100644 index 00000000000..90f32bfc163 --- /dev/null +++ b/apps/server/src/modules/media-source/domain/interface/media-source-sync-operation-report.ts @@ -0,0 +1,7 @@ +import { MediaSourceSyncOperation, MediaSourceSyncStatus } from '../../enum'; + +export interface MediaSourceSyncOperationReport { + operation: MediaSourceSyncOperation; + status: MediaSourceSyncStatus; + count: number; +} diff --git a/apps/server/src/modules/media-source/domain/interface/media-source-sync-report.ts b/apps/server/src/modules/media-source/domain/interface/media-source-sync-report.ts new file mode 100644 index 00000000000..252a6f2d542 --- /dev/null +++ b/apps/server/src/modules/media-source/domain/interface/media-source-sync-report.ts @@ -0,0 +1,9 @@ +import { MediaSourceSyncOperationReport } from './media-source-sync-operation-report'; + +export interface MediaSourceSyncReport { + totalCount: number; + successCount: number; + failedCount: number; + undeliveredCount: number; + operations: MediaSourceSyncOperationReport[]; +} diff --git a/apps/server/src/modules/media-source/enum/index.ts b/apps/server/src/modules/media-source/enum/index.ts index 7543244471a..7081bdfa253 100644 --- a/apps/server/src/modules/media-source/enum/index.ts +++ b/apps/server/src/modules/media-source/enum/index.ts @@ -1,2 +1,4 @@ export { MediaSourceDataFormat } from './media-source-data-format.enum'; export { MediaSourceAuthMethod } from './media-source-auth-method.enum'; +export { MediaSourceSyncOperation } from './media-source-sync-operation.enum'; +export { MediaSourceSyncStatus } from './media-source-sync-status.enum'; diff --git a/apps/server/src/modules/media-source/enum/media-source-sync-operation.enum.ts b/apps/server/src/modules/media-source/enum/media-source-sync-operation.enum.ts new file mode 100644 index 00000000000..d302ca147f4 --- /dev/null +++ b/apps/server/src/modules/media-source/enum/media-source-sync-operation.enum.ts @@ -0,0 +1,5 @@ +export enum MediaSourceSyncOperation { + ANY = 'any', + UPDATE = 'update', + CREATE = 'create', +} diff --git a/apps/server/src/modules/media-source/enum/media-source-sync-status.enum.ts b/apps/server/src/modules/media-source/enum/media-source-sync-status.enum.ts new file mode 100644 index 00000000000..6ff1dc27b70 --- /dev/null +++ b/apps/server/src/modules/media-source/enum/media-source-sync-status.enum.ts @@ -0,0 +1,5 @@ +export enum MediaSourceSyncStatus { + SUCCESS = 'success', + FAILED = 'failed', + UNDELIVERED = 'undelivered', +} diff --git a/apps/server/src/modules/media-source/media-source.module.ts b/apps/server/src/modules/media-source/media-source.module.ts index 2899e3cb458..c530aa69acb 100644 --- a/apps/server/src/modules/media-source/media-source.module.ts +++ b/apps/server/src/modules/media-source/media-source.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { MediaSourceRepo } from './repo'; -import { MediaSourceService } from './service'; +import { MediaSourceService, MediaSourceSyncService } from './service'; @Module({ - providers: [MediaSourceService, MediaSourceRepo], - exports: [MediaSourceService, MediaSourceRepo], + providers: [MediaSourceService, MediaSourceSyncService, MediaSourceRepo], + exports: [MediaSourceService, MediaSourceSyncService, MediaSourceRepo], }) export class MediaSourceModule {} diff --git a/apps/server/src/modules/media-source/service/index.ts b/apps/server/src/modules/media-source/service/index.ts index 95cc754abea..84e254d2035 100644 --- a/apps/server/src/modules/media-source/service/index.ts +++ b/apps/server/src/modules/media-source/service/index.ts @@ -1 +1,2 @@ export { MediaSourceService } from './media-source.service'; +export { MediaSourceSyncService } from './media-source-sync.service'; diff --git a/apps/server/src/modules/media-source/service/media-source-sync.service.spec.ts b/apps/server/src/modules/media-source/service/media-source-sync.service.spec.ts new file mode 100644 index 00000000000..4ee0af5ddec --- /dev/null +++ b/apps/server/src/modules/media-source/service/media-source-sync.service.spec.ts @@ -0,0 +1,28 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MediaSourceSyncService } from './media-source-sync.service'; + +describe(MediaSourceSyncService.name, () => { + let service: MediaSourceSyncService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MediaSourceSyncService], + }).compile(); + + service = module.get(MediaSourceSyncService); + }); + + describe('syncAllMediaMetadata', () => { + it('should return a sync report', async () => { + const result = await service.syncAllMediaMetadata(); + + expect(result).toMatchObject({ + totalCount: 0, + successCount: 0, + failedCount: 0, + undeliveredCount: 0, + operations: [], + }); + }); + }); +}); diff --git a/apps/server/src/modules/media-source/service/media-source-sync.service.ts b/apps/server/src/modules/media-source/service/media-source-sync.service.ts new file mode 100644 index 00000000000..7d326690f64 --- /dev/null +++ b/apps/server/src/modules/media-source/service/media-source-sync.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { MediaSourceSyncReport } from '../domain'; + +@Injectable() +export class MediaSourceSyncService { + public async syncAllMediaMetadata(): Promise { + const dummyReport: MediaSourceSyncReport = { + totalCount: 0, + successCount: 0, + failedCount: 0, + undeliveredCount: 0, + operations: [], + }; + + await Promise.resolve(); + + return dummyReport; + } +} diff --git a/apps/server/src/modules/media-source/testing/index.ts b/apps/server/src/modules/media-source/testing/index.ts index 9690d284c7d..399cd71c801 100644 --- a/apps/server/src/modules/media-source/testing/index.ts +++ b/apps/server/src/modules/media-source/testing/index.ts @@ -4,3 +4,5 @@ export { mediaSourceBasicAuthConfigFactory } from './media-source-basic-auth-con export { mediaSourceOauthConfigFactory } from './media-source-oauth-config.factory'; export { mediaSourceBasicConfigEmbeddableFactory } from './media-source-basic-auth-config.embeddable.factory'; export { mediaSourceOAuthConfigEmbeddableFactory } from './media-source-oauth-config.embeddable.factory'; +export { mediaSourceSyncReportFactory } from './media-source-sync-report.factory'; +export { mediaSourceSyncOperationReportFactory } from './media-source-sync-operation-report.factory'; diff --git a/apps/server/src/modules/media-source/testing/media-source-sync-operation-report.factory.ts b/apps/server/src/modules/media-source/testing/media-source-sync-operation-report.factory.ts new file mode 100644 index 00000000000..6240b895660 --- /dev/null +++ b/apps/server/src/modules/media-source/testing/media-source-sync-operation-report.factory.ts @@ -0,0 +1,13 @@ +import { Factory } from 'fishery'; +import { MediaSourceSyncOperationReport } from '../domain'; +import { MediaSourceSyncOperation, MediaSourceSyncStatus } from '../enum'; + +export const mediaSourceSyncOperationReportFactory = Factory.define(() => { + const syncOperationReportProps: MediaSourceSyncOperationReport = { + status: MediaSourceSyncStatus.SUCCESS, + operation: MediaSourceSyncOperation.CREATE, + count: 10, + }; + + return syncOperationReportProps; +}); diff --git a/apps/server/src/modules/media-source/testing/media-source-sync-report.factory.ts b/apps/server/src/modules/media-source/testing/media-source-sync-report.factory.ts new file mode 100644 index 00000000000..6f1ce187147 --- /dev/null +++ b/apps/server/src/modules/media-source/testing/media-source-sync-report.factory.ts @@ -0,0 +1,15 @@ +import { Factory } from 'fishery'; +import { MediaSourceSyncReport } from '../domain'; +import { mediaSourceSyncOperationReportFactory } from './media-source-sync-operation-report.factory'; + +export const mediaSourceSyncReportFactory = Factory.define(() => { + const syncReportProps: MediaSourceSyncReport = { + totalCount: 10, + successCount: 10, + failedCount: 0, + undeliveredCount: 0, + operations: mediaSourceSyncOperationReportFactory.buildList(1), + }; + + return syncReportProps; +}); diff --git a/package.json b/package.json index 32c125c655d..81c3495c571 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,9 @@ "nest:start:sync:vidis": "npm run nest:start:console -- sync run vidis", "nest:start:sync:vidis:dev": "npm run nest:start:console --watch -- sync run vidis", "nest:start:sync:vidis:debug": "npm run nest:start:console --watch --debug -- sync run vidis", + "nest:start:sync:media-metadata": "npm run nest:start:console -- sync run media-metadata", + "nest:start:sync:media-metadata:dev": "npm run nest:start:console --watch -- sync run media-metadata", + "nest:start:sync:media-metadata:debug": "npm run nest:start:console --watch --debug -- sync run media-metadata", "nest:test": "npm run nest:test:cov && npm run nest:lint", "nest:test:all": "jest \"^((?!(\\.load)\\.spec\\.ts).)*\"", "nest:test:unit": "jest \"^((?!(\\.api|\\.load)\\.spec\\.ts).)*\\.spec\\.ts$\"",