240 lines
9.3 KiB
TypeScript
240 lines
9.3 KiB
TypeScript
import type { MediaReference } from '@core/domain/media/MediaReference';
|
|
import type { RacingSuitColor } from '@core/media/domain/types/AvatarGenerationRequest';
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO';
|
|
import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO';
|
|
import type { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO';
|
|
import type { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
|
|
import type { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO';
|
|
import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO';
|
|
import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO';
|
|
import type { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO';
|
|
import type { UploadMediaOutputDTO } from './dtos/UploadMediaOutputDTO';
|
|
import type { ValidateFaceInputDTO } from './dtos/ValidateFaceInputDTO';
|
|
import type { ValidateFaceOutputDTO } from './dtos/ValidateFaceOutputDTO';
|
|
import type { MulterFile } from './types/MulterFile';
|
|
|
|
type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
|
|
type UploadMediaInput = UploadMediaInputDTO;
|
|
type UpdateAvatarInput = UpdateAvatarInputDTO;
|
|
|
|
// Use cases
|
|
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
|
|
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
|
|
import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase';
|
|
import { GetUploadedMediaUseCase, type GetUploadedMediaResult } from '@core/media/application/use-cases/GetUploadedMediaUseCase';
|
|
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
|
|
import { ResolveMediaReferenceUseCase } from '@core/media/application/use-cases/ResolveMediaReferenceUseCase';
|
|
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
|
|
import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase';
|
|
|
|
// Presenters (now transformers)
|
|
import { DeleteMediaPresenter } from './presenters/DeleteMediaPresenter';
|
|
import { GetAvatarPresenter } from './presenters/GetAvatarPresenter';
|
|
import { GetMediaPresenter } from './presenters/GetMediaPresenter';
|
|
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
|
|
import { UpdateAvatarPresenter } from './presenters/UpdateAvatarPresenter';
|
|
import { UploadMediaPresenter } from './presenters/UploadMediaPresenter';
|
|
|
|
import type { Logger } from '@core/shared/domain/Logger';
|
|
import {
|
|
DELETE_MEDIA_USE_CASE_TOKEN,
|
|
GET_AVATAR_USE_CASE_TOKEN,
|
|
GET_MEDIA_USE_CASE_TOKEN,
|
|
GET_UPLOADED_MEDIA_USE_CASE_TOKEN,
|
|
LOGGER_TOKEN,
|
|
REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
|
|
RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN,
|
|
UPDATE_AVATAR_USE_CASE_TOKEN,
|
|
UPLOAD_MEDIA_USE_CASE_TOKEN,
|
|
} from './MediaTokens';
|
|
|
|
@Injectable()
|
|
export class MediaService {
|
|
constructor(
|
|
@Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN)
|
|
private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase,
|
|
@Inject(UPLOAD_MEDIA_USE_CASE_TOKEN)
|
|
private readonly uploadMediaUseCase: UploadMediaUseCase,
|
|
@Inject(GET_MEDIA_USE_CASE_TOKEN)
|
|
private readonly getMediaUseCase: GetMediaUseCase,
|
|
@Inject(DELETE_MEDIA_USE_CASE_TOKEN)
|
|
private readonly deleteMediaUseCase: DeleteMediaUseCase,
|
|
@Inject(GET_AVATAR_USE_CASE_TOKEN)
|
|
private readonly getAvatarUseCase: GetAvatarUseCase,
|
|
@Inject(UPDATE_AVATAR_USE_CASE_TOKEN)
|
|
private readonly updateAvatarUseCase: UpdateAvatarUseCase,
|
|
@Inject(RESOLVE_MEDIA_REFERENCE_USE_CASE_TOKEN)
|
|
private readonly resolveMediaReferenceUseCase: ResolveMediaReferenceUseCase,
|
|
@Inject(GET_UPLOADED_MEDIA_USE_CASE_TOKEN)
|
|
private readonly getUploadedMediaUseCase: GetUploadedMediaUseCase,
|
|
@Inject(LOGGER_TOKEN)
|
|
private readonly logger: Logger,
|
|
private readonly requestAvatarGenerationPresenter: RequestAvatarGenerationPresenter,
|
|
private readonly uploadMediaPresenter: UploadMediaPresenter,
|
|
private readonly getMediaPresenter: GetMediaPresenter,
|
|
private readonly deleteMediaPresenter: DeleteMediaPresenter,
|
|
private readonly getAvatarPresenter: GetAvatarPresenter,
|
|
private readonly updateAvatarPresenter: UpdateAvatarPresenter,
|
|
) {}
|
|
|
|
async requestAvatarGeneration(
|
|
input: RequestAvatarGenerationInput,
|
|
): Promise<RequestAvatarGenerationOutputDTO> {
|
|
this.logger.debug('[MediaService] Requesting avatar generation.');
|
|
|
|
const result = await this.requestAvatarGenerationUseCase.execute({
|
|
userId: input.userId,
|
|
facePhotoData: input.facePhotoData,
|
|
suitColor: input.suitColor as RacingSuitColor,
|
|
});
|
|
|
|
if (result.isErr()) {
|
|
const error = result.unwrapErr();
|
|
return {
|
|
success: false,
|
|
requestId: '',
|
|
avatarUrls: [],
|
|
errorMessage: error.details?.message ?? 'Failed to request avatar generation',
|
|
};
|
|
}
|
|
|
|
return this.requestAvatarGenerationPresenter.transform(result.unwrap());
|
|
}
|
|
|
|
async uploadMedia(
|
|
input: UploadMediaInput & { file: MulterFile } & { userId?: string; metadata?: Record<string, unknown> },
|
|
): Promise<UploadMediaOutputDTO> {
|
|
this.logger.debug('[MediaService] Uploading media.');
|
|
|
|
const result = await this.uploadMediaUseCase.execute({
|
|
file: input.file,
|
|
uploadedBy: input.userId ?? '',
|
|
metadata: input.metadata || {},
|
|
});
|
|
|
|
if (result.isErr()) {
|
|
const error = result.unwrapErr();
|
|
return {
|
|
success: false,
|
|
error: error.details?.message ?? 'Upload failed',
|
|
};
|
|
}
|
|
|
|
return this.uploadMediaPresenter.transform(result.unwrap());
|
|
}
|
|
|
|
async getMedia(mediaId: string): Promise<GetMediaOutputDTO | null> {
|
|
this.logger.debug(`[MediaService] Getting media: ${mediaId}`);
|
|
|
|
const result = await this.getMediaUseCase.execute({ mediaId });
|
|
|
|
if (result.isErr()) {
|
|
const error = result.unwrapErr();
|
|
if (error.code === 'MEDIA_NOT_FOUND') {
|
|
return null;
|
|
}
|
|
throw new Error(error.details?.message ?? 'Failed to get media');
|
|
}
|
|
|
|
return this.getMediaPresenter.transform(result.unwrap());
|
|
}
|
|
|
|
async deleteMedia(mediaId: string): Promise<DeleteMediaOutputDTO> {
|
|
this.logger.debug(`[MediaService] Deleting media: ${mediaId}`);
|
|
|
|
const result = await this.deleteMediaUseCase.execute({ mediaId });
|
|
|
|
if (result.isErr()) {
|
|
const error = result.unwrapErr();
|
|
return {
|
|
success: false,
|
|
error: error.details?.message ?? 'Failed to delete media',
|
|
};
|
|
}
|
|
|
|
return this.deleteMediaPresenter.transform(result.unwrap());
|
|
}
|
|
|
|
async getAvatar(driverId: string): Promise<GetAvatarOutputDTO | null> {
|
|
this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`);
|
|
|
|
const result = await this.getAvatarUseCase.execute({ driverId });
|
|
|
|
if (result.isErr()) {
|
|
const error = result.unwrapErr();
|
|
if (error.code === 'AVATAR_NOT_FOUND') {
|
|
return null;
|
|
}
|
|
throw new Error(error.details?.message ?? 'Failed to get avatar');
|
|
}
|
|
|
|
return this.getAvatarPresenter.transform(result.unwrap());
|
|
}
|
|
|
|
async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutputDTO> {
|
|
this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`);
|
|
|
|
// Handle null avatarUrl - this would mean removing the avatar
|
|
if (input.avatarUrl === null) {
|
|
// For now, we'll treat null as an error since the use case requires a URL
|
|
// In a complete implementation, this would trigger avatar removal
|
|
return {
|
|
success: false,
|
|
error: 'Avatar URL cannot be null',
|
|
};
|
|
}
|
|
|
|
const result = await this.updateAvatarUseCase.execute({
|
|
driverId,
|
|
mediaUrl: input.avatarUrl,
|
|
});
|
|
|
|
if (result.isErr()) {
|
|
const error = result.unwrapErr();
|
|
return {
|
|
success: false,
|
|
error: error.details?.message ?? 'Failed to update avatar',
|
|
};
|
|
}
|
|
|
|
return this.updateAvatarPresenter.transform(result.unwrap());
|
|
}
|
|
|
|
async validateFacePhoto(input: ValidateFaceInputDTO): Promise<ValidateFaceOutputDTO> {
|
|
this.logger.debug('[MediaService] Validating face photo.');
|
|
|
|
// Simple validation: check if it's a valid base64 image
|
|
if (!input.imageData || !input.imageData.startsWith('data:image/')) {
|
|
return { isValid: false, errorMessage: 'Invalid image data' };
|
|
}
|
|
|
|
// Check file size (rough estimate from base64 length)
|
|
const base64Length = input.imageData.length;
|
|
const fileSizeInBytes = (base64Length * 3) / 4; // Rough estimate
|
|
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
|
|
if (fileSizeInBytes > maxSize) {
|
|
return { isValid: false, errorMessage: 'Image too large (max 5MB)' };
|
|
}
|
|
|
|
return { isValid: true };
|
|
}
|
|
|
|
async resolveMediaReference(reference: MediaReference): Promise<string | null> {
|
|
const result = await this.resolveMediaReferenceUseCase.execute({ reference });
|
|
if (result.isErr()) {
|
|
throw new Error(result.unwrapErr().message);
|
|
}
|
|
return result.unwrap();
|
|
}
|
|
|
|
async getUploadedMedia(storageKey: string): Promise<GetUploadedMediaResult | null> {
|
|
const result = await this.getUploadedMediaUseCase.execute({ storageKey });
|
|
if (result.isErr()) {
|
|
throw new Error(result.unwrapErr().message);
|
|
}
|
|
return result.unwrap();
|
|
}
|
|
}
|