diff --git a/server/src/config.ts b/server/src/config.ts index 8711e72467e2b0..90d4434a5f1871 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -12,7 +12,7 @@ import { VideoContainer, } from 'src/enum'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; -import { ImageOptions } from 'src/interfaces/media.interface'; +import { ImageOptions } from 'src/types'; export interface SystemConfig { backup: { diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index ffc0d953d7d439..197ab5f92d7590 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -10,7 +10,6 @@ import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; import { INotificationRepository } from 'src/interfaces/notification.interface'; @@ -77,6 +76,7 @@ export const repositories = [ AuditRepository, ApiKeyRepository, ConfigRepository, + MediaRepository, MemoryRepository, ViewRepository, ]; @@ -94,7 +94,6 @@ export const providers = [ { provide: ILoggerRepository, useClass: LoggerRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, { provide: IMapRepository, useClass: MapRepository }, - { provide: IMediaRepository, useClass: MediaRepository }, { provide: IMetadataRepository, useClass: MetadataRepository }, { provide: IMoveRepository, useClass: MoveRepository }, { provide: INotificationRepository, useClass: NotificationRepository }, diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index ffd194e9265b22..2ad304fb21874b 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -12,12 +12,11 @@ import { DecodeToBufferOptions, GenerateThumbhashOptions, GenerateThumbnailOptions, - IMediaRepository, ImageDimensions, ProbeOptions, TranscodeCommand, VideoInfo, -} from 'src/interfaces/media.interface'; +} from 'src/types'; import { handlePromiseError } from 'src/utils/misc'; const probe = (input: string, options: string[]): Promise => @@ -37,7 +36,7 @@ type ProgressEvent = { }; @Injectable() -export class MediaRepository implements IMediaRepository { +export class MediaRepository { constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) { this.logger.setContext(MediaRepository.name); } diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 1a20b4c97e7f2c..af46146e3e7c73 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -18,7 +18,6 @@ import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; import { INotificationRepository } from 'src/interfaces/notification.interface'; @@ -43,6 +42,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { ViewRepository } from 'src/repositories/view-repository'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; @@ -69,7 +69,7 @@ export class BaseService { @Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository, @Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository, @Inject(IMapRepository) protected mapRepository: IMapRepository, - @Inject(IMediaRepository) protected mediaRepository: IMediaRepository, + protected mediaRepository: MediaRepository, protected memoryRepository: MemoryRepository, @Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository, @Inject(IMoveRepository) protected moveRepository: IMoveRepository, diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 1784428d3120f0..a0f296a27ce14d 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,3 +1,4 @@ +import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; @@ -15,12 +16,12 @@ import { import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IJobRepository, JobCounts, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMediaRepository, RawImageInfo } from 'src/interfaces/media.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { MediaService } from 'src/services/media.service'; +import { IMediaRepository, RawImageInfo } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { probeStub } from 'test/fixtures/media.stub'; @@ -257,7 +258,7 @@ describe(MediaService.name, () => { beforeEach(() => { rawBuffer = Buffer.from('image data'); rawInfo = { width: 100, height: 100, channels: 3 }; - mediaMock.decodeImage.mockResolvedValue({ data: rawBuffer, info: rawInfo }); + mediaMock.decodeImage.mockResolvedValue({ data: rawBuffer, info: rawInfo as OutputInfo }); }); it('should skip thumbnail generation if asset not found', async () => { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index b4bfd785fded6f..81c325e7ea081f 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -27,8 +27,8 @@ import { JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { AudioStreamInfo, VideoFormat, VideoInterfaces, VideoStreamInfo } from 'src/interfaces/media.interface'; import { BaseService } from 'src/services/base.service'; +import { AudioStreamInfo, VideoFormat, VideoInterfaces, VideoStreamInfo } from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 6617ec9e2492b0..4688e8b119a008 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -11,7 +11,6 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -19,7 +18,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { MetadataService } from 'src/services/metadata.service'; -import { IConfigRepository } from 'src/types'; +import { IConfigRepository, IMediaRepository } from 'src/types'; import { assetStub } from 'test/fixtures/asset.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 3f454f178fd0e8..dc9d7a9329e92e 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -7,12 +7,12 @@ import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interfac import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { DetectedFaces, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { PersonService } from 'src/services/person.service'; +import { IMediaRepository } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index b1a7ec75a25f14..bcc65cfad3236d 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -42,9 +42,9 @@ import { QueueName, } from 'src/interfaces/job.interface'; import { BoundingBox } from 'src/interfaces/machine-learning.interface'; -import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface'; import { UpdateFacesData } from 'src/interfaces/person.interface'; import { BaseService } from 'src/services/base.service'; +import { CropOptions, ImageDimensions, InputDimensions } from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; diff --git a/server/src/types.ts b/server/src/types.ts index 42e1b8e94b3d71..97f866e924f789 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,10 +1,11 @@ import { UserEntity } from 'src/entities/user.entity'; -import { Permission } from 'src/enum'; +import { ExifOrientation, ImageFormat, Permission, TranscodeTarget, VideoCodec } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { ViewRepository } from 'src/repositories/view-repository'; @@ -24,6 +25,7 @@ export type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInter export type IApiKeyRepository = RepositoryInterface; export type IAuditRepository = RepositoryInterface; export type IConfigRepository = RepositoryInterface; +export type IMediaRepository = RepositoryInterface; export type IMemoryRepository = RepositoryInterface; export type IViewRepository = RepositoryInterface; @@ -39,3 +41,135 @@ export type ApiKeyItem = export type MemoryItem = | Awaited> | Awaited>[0]; + +export interface CropOptions { + top: number; + left: number; + width: number; + height: number; +} + +export interface ImageOptions { + format: ImageFormat; + quality: number; + size: number; +} + +export interface RawImageInfo { + width: number; + height: number; + channels: 1 | 2 | 3 | 4; +} + +interface DecodeImageOptions { + colorspace: string; + crop?: CropOptions; + processInvalidImages: boolean; + raw?: RawImageInfo; +} + +export interface DecodeToBufferOptions extends DecodeImageOptions { + size: number; + orientation?: ExifOrientation; +} + +export type GenerateThumbnailOptions = ImageOptions & DecodeImageOptions; + +export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo }; + +export type GenerateThumbhashOptions = DecodeImageOptions; + +export type GenerateThumbhashFromBufferOptions = GenerateThumbhashOptions & { raw: RawImageInfo }; + +export interface GenerateThumbnailsOptions { + colorspace: string; + crop?: CropOptions; + preview?: ImageOptions; + processInvalidImages: boolean; + thumbhash?: boolean; + thumbnail?: ImageOptions; +} + +export interface VideoStreamInfo { + index: number; + height: number; + width: number; + rotation: number; + codecName?: string; + frameCount: number; + isHDR: boolean; + bitrate: number; + pixelFormat: string; +} + +export interface AudioStreamInfo { + index: number; + codecName?: string; + frameCount: number; +} + +export interface VideoFormat { + formatName?: string; + formatLongName?: string; + duration: number; + bitrate: number; +} + +export interface ImageDimensions { + width: number; + height: number; +} + +export interface InputDimensions extends ImageDimensions { + inputPath: string; +} + +export interface VideoInfo { + format: VideoFormat; + videoStreams: VideoStreamInfo[]; + audioStreams: AudioStreamInfo[]; +} + +export interface TranscodeCommand { + inputOptions: string[]; + outputOptions: string[]; + twoPass: boolean; + progress: { + frameCount: number; + percentInterval: number; + }; +} + +export interface BitrateDistribution { + max: number; + target: number; + min: number; + unit: string; +} + +export interface ImageBuffer { + data: Buffer; + info: RawImageInfo; +} + +export interface VideoCodecSWConfig { + getCommand( + target: TranscodeTarget, + videoStream: VideoStreamInfo, + audioStream: AudioStreamInfo, + format?: VideoFormat, + ): TranscodeCommand; +} + +export interface VideoCodecHWConfig extends VideoCodecSWConfig { + getSupportedCodecs(): Array; +} + +export interface ProbeOptions { + countFrames: boolean; +} + +export interface VideoInterfaces { + dri: string[]; + mali: boolean; +} diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index cd8eedba258b03..62cf6500fbb6c4 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -9,7 +9,7 @@ import { VideoFormat, VideoInterfaces, VideoStreamInfo, -} from 'src/interfaces/media.interface'; +} from 'src/types'; export class BaseConfig implements VideoCodecSWConfig { readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index de11c23f0a9618..021f899ae55b63 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -1,4 +1,4 @@ -import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/interfaces/media.interface'; +import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/types'; const probeStubDefaultFormat: VideoFormat = { formatName: 'mov,mp4,m4a,3gp,3g2,mj2', diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index a809b081623476..1e909dcae31bb2 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -1,4 +1,4 @@ -import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMediaRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newMediaRepositoryMock = (): Mocked => { diff --git a/server/test/utils.ts b/server/test/utils.ts index 01fb1f746518f9..2ad14d5ee94b9b 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -7,6 +7,7 @@ import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; import { MemoryRepository } from 'src/repositories/memory.repository'; import { ViewRepository } from 'src/repositories/view-repository'; import { BaseService } from 'src/services/base.service'; @@ -15,6 +16,7 @@ import { IActivityRepository, IApiKeyRepository, IAuditRepository, + IMediaRepository, IMemoryRepository, IViewRepository, } from 'src/types'; @@ -133,7 +135,7 @@ export const newTestService = ( libraryMock, machineLearningMock, mapMock, - mediaMock, + mediaMock as IMediaRepository as MediaRepository, memoryMock as IMemoryRepository as MemoryRepository, metadataMock, moveMock,