Support uploaded cover images for media posts
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-05-25 21:42:27 +03:00
الأصل 8863f61d00
التزام efd87659b2
4 ملفات معدلة مع 104 إضافات و48 حذوفات

عرض الملف

@@ -1898,9 +1898,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/video-cover.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -1964,7 +1964,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('video');",
"pm.expect(json.durationSeconds).to.eql(42);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/video-cover.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Sharqi');",
"pm.expect(json.maqam).to.eql('Hijaz');",
"pm.expect(json.rhythmSignature).to.eql('6/8');",
@@ -2005,9 +2005,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/reel-cover.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -2051,7 +2051,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('video');",
"pm.expect(json.durationSeconds).to.eql(30);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/reel-cover.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Sharqi');",
"pm.expect(json.maqam).to.eql('Hijaz');",
"pm.expect(json.rhythmSignature).to.eql('6/8');",
@@ -2146,9 +2146,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/audio-cover.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -2192,7 +2192,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('audio');",
"pm.expect(json.durationSeconds).to.eql(54);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/audio-cover.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Sharqi');",
"pm.expect(json.maqam).to.eql('Hijaz');",
"pm.expect(json.rhythmSignature).to.eql('6/8');",
@@ -2404,9 +2404,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/video-cover-updated.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -2453,7 +2453,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('video');",
"pm.expect(json.durationSeconds).to.eql(58);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/video-cover-updated.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Contemporary');",
"pm.expect(json.maqam).to.eql('Bayati');",
"pm.expect(json.rhythmSignature).to.eql('4/4');"
@@ -2487,9 +2487,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/audio-cover-updated.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -2536,7 +2536,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('audio');",
"pm.expect(json.durationSeconds).to.eql(61);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/audio-cover-updated.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Tarab');",
"pm.expect(json.maqam).to.eql('Nahawand');",
"pm.expect(json.rhythmSignature).to.eql('6/8');"
@@ -8606,4 +8606,4 @@
"value": ""
}
]
}
}

عرض الملف

@@ -1359,9 +1359,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/video-cover.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -1425,7 +1425,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('video');",
"pm.expect(json.durationSeconds).to.eql(42);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/video-cover.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Sharqi');",
"pm.expect(json.maqam).to.eql('Hijaz');",
"pm.expect(json.rhythmSignature).to.eql('6/8');",
@@ -1466,9 +1466,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/reel-cover.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -1512,7 +1512,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('video');",
"pm.expect(json.durationSeconds).to.eql(30);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/reel-cover.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Sharqi');",
"pm.expect(json.maqam).to.eql('Hijaz');",
"pm.expect(json.rhythmSignature).to.eql('6/8');",
@@ -1607,9 +1607,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/audio-cover.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -1653,7 +1653,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('audio');",
"pm.expect(json.durationSeconds).to.eql(54);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/audio-cover.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Sharqi');",
"pm.expect(json.maqam).to.eql('Hijaz');",
"pm.expect(json.rhythmSignature).to.eql('6/8');",
@@ -1865,9 +1865,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/video-cover-updated.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -1914,7 +1914,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('video');",
"pm.expect(json.durationSeconds).to.eql(58);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/video-cover-updated.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Contemporary');",
"pm.expect(json.maqam).to.eql('Bayati');",
"pm.expect(json.rhythmSignature).to.eql('4/4');"
@@ -1948,9 +1948,9 @@
"type": "text"
},
{
"key": "thumbnailUrl",
"value": "https://cdn.example.com/audio-cover-updated.jpg",
"type": "text"
"key": "coverImageFile",
"type": "file",
"src": []
},
{
"key": "style",
@@ -1997,7 +1997,7 @@
"const json = pm.response.json();",
"pm.expect(json.postType).to.eql('audio');",
"pm.expect(json.durationSeconds).to.eql(61);",
"pm.expect(json.thumbnailUrl).to.eql('https://cdn.example.com/audio-cover-updated.jpg');",
"if (json.thumbnailUrl) { pm.expect(json.thumbnailUrl).to.be.a('string'); }",
"pm.expect(json.style).to.eql('Tarab');",
"pm.expect(json.maqam).to.eql('Nahawand');",
"pm.expect(json.rhythmSignature).to.eql('6/8');"
@@ -5832,4 +5832,4 @@
"value": ""
}
]
}
}

عرض الملف

@@ -45,6 +45,7 @@ export class PostsController {
{ name: 'imageFiles', maxCount: 10 },
{ name: 'videoFile', maxCount: 1 },
{ name: 'audioFile', maxCount: 1 },
{ name: 'coverImageFile', maxCount: 1 },
]),
)
@ApiConsumes('multipart/form-data')
@@ -76,6 +77,7 @@ export class PostsController {
imageFiles: { type: 'array', items: { type: 'string', format: 'binary' } },
videoFile: { type: 'string', format: 'binary' },
audioFile: { type: 'string', format: 'binary' },
coverImageFile: { type: 'string', format: 'binary' },
},
},
})
@@ -88,6 +90,7 @@ export class PostsController {
imageFiles?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
videoFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
audioFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
coverImageFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
},
) {
return this.postsService.create(
@@ -96,6 +99,7 @@ export class PostsController {
files?.imageFiles ?? [],
files?.videoFile?.[0],
files?.audioFile?.[0],
files?.coverImageFile?.[0],
);
}
@@ -119,6 +123,7 @@ export class PostsController {
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'videoFile', maxCount: 1 },
{ name: 'coverImageFile', maxCount: 1 },
]),
)
@ApiConsumes('multipart/form-data')
@@ -136,6 +141,7 @@ export class PostsController {
rhythmSignature: { type: 'string', example: '6/8' },
mentionUsernames: { type: 'array', items: { type: 'string' } },
videoFile: { type: 'string', format: 'binary' },
coverImageFile: { type: 'string', format: 'binary' },
},
},
})
@@ -146,9 +152,10 @@ export class PostsController {
@UploadedFiles()
files?: {
videoFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
coverImageFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
},
) {
return this.postsService.createReel(user.sub, dto, files?.videoFile?.[0]);
return this.postsService.createReel(user.sub, dto, files?.videoFile?.[0], files?.coverImageFile?.[0]);
}
@ApiBearerAuth()
@@ -172,6 +179,7 @@ export class PostsController {
{ name: 'imageFiles', maxCount: 10 },
{ name: 'videoFile', maxCount: 1 },
{ name: 'audioFile', maxCount: 1 },
{ name: 'coverImageFile', maxCount: 1 },
]),
)
@ApiConsumes('multipart/form-data')
@@ -203,6 +211,7 @@ export class PostsController {
imageFiles: { type: 'array', items: { type: 'string', format: 'binary' } },
videoFile: { type: 'string', format: 'binary' },
audioFile: { type: 'string', format: 'binary' },
coverImageFile: { type: 'string', format: 'binary' },
},
},
})
@@ -216,6 +225,7 @@ export class PostsController {
imageFiles?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
videoFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
audioFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
coverImageFile?: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>;
},
) {
return this.postsService.update(
@@ -225,6 +235,7 @@ export class PostsController {
files?.imageFiles ?? [],
files?.videoFile?.[0],
files?.audioFile?.[0],
files?.coverImageFile?.[0],
);
}

عرض الملف

@@ -87,6 +87,7 @@ export class PostsService {
imageFiles: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }> = [],
videoFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
audioFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
coverImageFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
): Promise<PostDocument> {
const inputImageUrls = dto.imageUrls ?? [];
if (inputImageUrls.length > 10 || imageFiles.length > 10) {
@@ -104,6 +105,12 @@ export class PostsService {
if (audioFile && dto.audioUrl) {
throw new BadRequestException('Provide either audioFile or audioUrl, not both');
}
if (coverImageFile && dto.thumbnailUrl) {
throw new BadRequestException('Provide either coverImageFile or thumbnailUrl, not both');
}
if (coverImageFile && !(videoFile || audioFile || dto.videoUrl || dto.audioUrl)) {
throw new BadRequestException('coverImageFile is allowed only with video or audio posts');
}
if ((videoFile || dto.videoUrl) && (imageFiles.length || inputImageUrls.length)) {
throw new BadRequestException('Post can contain either images or video, not both');
}
@@ -115,10 +122,15 @@ export class PostsService {
const uploadedImageUrls = savedImageUploads.map((item) => item.primaryUrl);
const uploadedImageVariants = savedImageUploads.map((item) => item.variants);
const savedVideoUpload = videoFile ? await this.saveVideoUpload(videoFile) : null;
const savedCoverImageUpload = coverImageFile
? await this.saveResponsiveImageAsset('thumbnails', coverImageFile)
: null;
const uploadedVideoUrl = savedVideoUpload?.videoUrl ?? '';
const uploadedHlsUrl = savedVideoUpload?.hlsUrl ?? '';
const uploadedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
const uploadedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
const generatedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
const generatedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
const uploadedThumbnailUrl = savedCoverImageUpload?.primaryUrl ?? generatedThumbnailUrl;
const uploadedThumbnailVariants = savedCoverImageUpload?.variants ?? generatedThumbnailVariants;
const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
const finalImageUrls = uploadedImageUrls.length ? uploadedImageUrls : inputImageUrls;
const finalImageVariants = uploadedImageVariants.length ? uploadedImageVariants : [];
@@ -174,11 +186,18 @@ export class PostsService {
uploadedThumbnailUrl
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
: Promise.resolve(),
generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl
? this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants)
: Promise.resolve(),
uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
]);
throw error;
}
if (generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl) {
await this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants);
}
await this.usersRepository.incrementPostsCount(userId, 1);
await this.feedVersionService.bumpGlobalVersion();
await this.notifyMentionedUsers(userId, post.id, mentionResolution.mentionedUsers, finalContent);
@@ -192,6 +211,7 @@ export class PostsService {
imageFiles: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }> = [],
videoFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
audioFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
coverImageFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
): Promise<PostDocument> {
const post = await this.postsRepository.findById(postId);
if (!post) {
@@ -219,6 +239,15 @@ export class PostsService {
if (audioFile && dto.audioUrl) {
throw new BadRequestException('Provide either audioFile or audioUrl, not both');
}
if (coverImageFile && dto.thumbnailUrl) {
throw new BadRequestException('Provide either coverImageFile or thumbnailUrl, not both');
}
if (coverImageFile && (imageFiles.length || inputImageUrls.length)) {
throw new BadRequestException('coverImageFile is allowed only with video or audio posts');
}
if (coverImageFile && !(videoFile || audioFile || dto.videoUrl || dto.audioUrl || post.videoUrl || post.audioUrl)) {
throw new BadRequestException('coverImageFile is allowed only with video or audio posts');
}
if ((videoFile || dto.videoUrl) && (imageFiles.length || inputImageUrls.length)) {
throw new BadRequestException('Post can contain either images or video, not both');
}
@@ -230,10 +259,15 @@ export class PostsService {
const uploadedImageUrls = savedImageUploads.map((item) => item.primaryUrl);
const uploadedImageVariants = savedImageUploads.map((item) => item.variants);
const savedVideoUpload = videoFile ? await this.saveVideoUpload(videoFile) : null;
const savedCoverImageUpload = coverImageFile
? await this.saveResponsiveImageAsset('thumbnails', coverImageFile)
: null;
const uploadedVideoUrl = savedVideoUpload?.videoUrl ?? '';
const uploadedHlsUrl = savedVideoUpload?.hlsUrl ?? '';
const uploadedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
const uploadedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
const generatedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
const generatedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
const uploadedThumbnailUrl = savedCoverImageUpload?.primaryUrl ?? generatedThumbnailUrl;
const uploadedThumbnailVariants = savedCoverImageUpload?.variants ?? generatedThumbnailVariants;
const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
const hasImageUpdate = typeof dto.imageUrls !== 'undefined' || imageFiles.length > 0;
const existingImageVariants = Array.isArray((post as any).imageVariants)
@@ -266,7 +300,7 @@ export class PostsService {
? uploadedAudioUrl
: dto.audioUrl ?? ''
: post.audioUrl ?? '';
const nextThumbnailVariants = videoFile
const nextThumbnailVariants = coverImageFile || videoFile
? uploadedThumbnailVariants
: typeof dto.thumbnailUrl === 'string'
? null
@@ -392,6 +426,9 @@ export class PostsService {
uploadedThumbnailUrl
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
: Promise.resolve(),
generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl
? this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants)
: Promise.resolve(),
uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
]);
throw error;
@@ -404,6 +441,9 @@ export class PostsService {
uploadedThumbnailUrl
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
: Promise.resolve(),
generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl
? this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants)
: Promise.resolve(),
uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
]);
throw new NotFoundException('Post not found');
@@ -418,6 +458,9 @@ export class PostsService {
if ((post.thumbnailUrl ?? '') !== (updated.thumbnailUrl ?? '')) {
await this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants);
}
if (generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl) {
await this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants);
}
if (hasAudioUpdate && (post.audioUrl ?? '') !== (updated.audioUrl ?? '')) {
await this.deleteManagedPostMedia(post.audioUrl ?? '');
}
@@ -567,6 +610,7 @@ export class PostsService {
userId: string,
dto: CreateReelDto,
videoFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
coverImageFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
): Promise<PostDocument> {
if (!videoFile && !dto.videoUrl) {
throw new BadRequestException('Reel requires videoFile or videoUrl');
@@ -592,6 +636,7 @@ export class PostsService {
[],
videoFile,
undefined,
coverImageFile,
);
}