import { ApiProperty } from '@nestjs/swagger'; import { UploadSessionStatus } from '@prisma/client'; import { Type } from 'class-transformer'; import { IsInt, IsOptional, IsString, IsUUID, Matches, Max, Min, MinLength, ValidateNested, } from 'class-validator'; export class UploadPrepareRequestDto { @ApiProperty({ format: 'uuid', required: false, description: 'Optional client metadata. Authorization: Bearer is required and determines access.', }) @IsOptional() @IsUUID() deviceId?: string; @ApiProperty({ example: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }) @Matches(/^[a-f0-9]{64}$/) sha256!: string; @ApiProperty({ example: 'track.mp3' }) @IsString() @Matches(/\.mp3$/i) originalFilename!: string; @ApiProperty({ example: 10485760 }) @IsInt() @Min(1) @Max(Number.MAX_SAFE_INTEGER) sizeBytes!: number; } export class UploadPrepareResponseDto { @ApiProperty({ enum: ['exists', 'upload_required'] }) status!: 'exists' | 'upload_required'; @ApiProperty({ required: false, format: 'uuid' }) uploadId?: string; @ApiProperty({ required: false, example: 0 }) nextOffset?: number; @ApiProperty({ required: false, format: 'uuid' }) trackId?: string; @ApiProperty({ required: false, format: 'uuid' }) assetId?: string; } export class UploadSessionStatusResponseDto { @ApiProperty({ format: 'uuid' }) uploadId!: string; @ApiProperty({ enum: UploadSessionStatus }) status!: UploadSessionStatus; @ApiProperty({ example: 0 }) receivedBytes!: string; @ApiProperty({ example: 10485760 }) expectedSizeBytes!: string; @ApiProperty({ example: 0 }) nextOffset!: string; @ApiProperty({ required: false, example: '2026-05-28T12:00:00.000Z' }) finalizedAt?: string; } export class UploadFinalizeArtworkDto { @ApiProperty({ example: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ...' }) @IsString() @MinLength(1) dataBase64!: string; @ApiProperty({ example: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', }) @Matches(/^[a-f0-9]{64}$/) sha256!: string; @ApiProperty({ enum: ['image/jpeg', 'image/png'] }) @Matches(/^image\/(jpeg|png)$/) mimeType!: string; @ApiProperty({ required: false, example: 512 }) @IsOptional() @IsInt() @Min(1) @Max(Number.MAX_SAFE_INTEGER) width?: number; @ApiProperty({ required: false, example: 512 }) @IsOptional() @IsInt() @Min(1) @Max(Number.MAX_SAFE_INTEGER) height?: number; } export class UploadFinalizeRequestDto { static readonly artworkMimeTypes = ['image/jpeg', 'image/png'] as const; @ApiProperty({ example: 'Track Title' }) @IsString() @MinLength(1) title!: string; @ApiProperty({ example: 'Track Artist' }) @IsString() @MinLength(1) artist!: string; @ApiProperty({ required: false, example: 'Album Title' }) @IsOptional() @IsString() album?: string; @ApiProperty({ required: false, example: 245000 }) @IsOptional() @IsInt() @Min(1) @Max(Number.MAX_SAFE_INTEGER) durationMs?: number; @ApiProperty({ required: false, type: () => UploadFinalizeArtworkDto, }) @IsOptional() @ValidateNested() @Type(() => UploadFinalizeArtworkDto) artwork?: UploadFinalizeArtworkDto; } export class UploadFinalizeResponseDto { @ApiProperty({ format: 'uuid' }) trackId!: string; @ApiProperty({ format: 'uuid' }) assetId!: string; }