Harden media health checks and duration extraction
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-05-31 18:53:07 +03:00
الأصل 1973b8b904
التزام 637782aed6
11 ملفات معدلة مع 587 إضافات و138 حذوفات

عرض الملف

@@ -1,12 +1,9 @@
import {
BadGatewayException,
Injectable,
ServiceUnavailableException,
} from '@nestjs/common';
import { BadGatewayException, Injectable, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleAuth } from 'google-auth-library';
import { generateWaveformPeaksFromBuffer } from '../../common/utils/waveform.util';
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
import { MediaProbeService } from '../../infrastructure/storage/media-probe.service';
import { TextToMusicDto } from './dto/text-to-music.dto';
@Injectable()
@@ -14,29 +11,84 @@ export class MediaService {
constructor(
private readonly configService: ConfigService,
private readonly storageService: ManagedStorageService,
private readonly mediaProbeService: MediaProbeService,
) {}
async getMediaHealth() {
const [storageHealth, ffmpeg, ffprobe] = await Promise.all([
this.storageService.getHealth(),
this.mediaProbeService.checkFfmpeg(),
this.mediaProbeService.checkFfprobe(),
]);
const storageProvider =
this.configService.get<string>('storage.provider', { infer: true }) ?? 'local';
const publicBaseUrl = this.configService.get<string>('publicBaseUrl', { infer: true }) ?? '';
const warnings: string[] = [];
const imageProcessingEnabled =
this.configService.get<boolean>('imageProcessing.enabled', { infer: true }) ?? false;
const videoProcessingEnabled =
this.configService.get<boolean>('videoProcessing.enabled', { infer: true }) ?? false;
const videoHlsGenerationEnabled =
this.configService.get<boolean>('videoProcessing.generateHls', { infer: true }) ?? true;
const videoThumbnailGenerationEnabled =
this.configService.get<boolean>('videoProcessing.generateThumbnails', { infer: true }) ??
true;
const audioProcessingEnabled =
this.configService.get<boolean>('audioProcessing.enabled', { infer: true }) ?? false;
if (storageProvider === 'local') {
warnings.push(
'Local storage requires persistent volume mounted to /app/uploads in production',
);
}
if (!publicBaseUrl) {
warnings.push('PUBLIC_BASE_URL is not configured');
}
if ((imageProcessingEnabled || videoProcessingEnabled) && !ffmpeg.available) {
warnings.push('ffmpeg is not available; image/video processing may fail');
}
if (!ffprobe.available) {
warnings.push('ffprobe is not available; duration extraction may fail');
}
if (storageProvider === 's3' && !(storageHealth.isS3Configured ?? storageHealth.s3Configured)) {
warnings.push('S3 provider selected but missing required env variables');
}
const storageWritable =
storageProvider !== 'local' || storageHealth.uploadPathWritable !== false;
const status = !storageWritable ? 'error' : warnings.length ? 'warning' : 'ok';
return {
storage: await this.storageService.getHealth(),
status,
storage: storageHealth,
processing: {
imageProcessingEnabled:
this.configService.get<boolean>('imageProcessing.enabled', { infer: true }) ?? false,
videoProcessingEnabled:
this.configService.get<boolean>('videoProcessing.enabled', { infer: true }) ?? false,
videoHlsGenerationEnabled:
this.configService.get<boolean>('videoProcessing.generateHls', { infer: true }) ?? true,
videoThumbnailGenerationEnabled:
this.configService.get<boolean>('videoProcessing.generateThumbnails', { infer: true }) ??
true,
ffmpegPath:
this.configService.get<string>('videoProcessing.ffmpegPath', { infer: true }) ?? 'ffmpeg',
imageProcessingEnabled,
videoProcessingEnabled,
videoHlsGenerationEnabled,
videoThumbnailGenerationEnabled,
audioProcessingEnabled,
ffmpegPath: ffmpeg.path,
ffmpegAvailable: ffmpeg.available,
ffmpegVersion: ffmpeg.version,
ffprobePath: ffprobe.path,
ffprobeAvailable: ffprobe.available,
ffprobeVersion: ffprobe.version,
ffmpeg,
ffprobe,
},
serving: {
rangeRequests: true,
immutableCacheSeconds: 31536000,
hlsManifestCacheSeconds: 300,
},
staticServing: {
uploadsPublicPath:
storageHealth.uploadsPublicPath ?? storageHealth.publicPath ?? '/uploads',
rangeRequestsExpected: true,
cacheHeadersExpected: true,
hlsMimeExpected: 'application/vnd.apple.mpegurl',
},
warnings,
};
}
@@ -67,7 +119,7 @@ export class MediaService {
const client = await auth.getClient();
const accessTokenRaw = await client.getAccessToken();
const accessToken =
typeof accessTokenRaw === 'string' ? accessTokenRaw : accessTokenRaw?.token ?? '';
typeof accessTokenRaw === 'string' ? accessTokenRaw : (accessTokenRaw?.token ?? '');
if (!accessToken) {
throw new ServiceUnavailableException('Failed to authenticate with Google Cloud');

عرض الملف

@@ -1,4 +1,10 @@
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
ForbiddenException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { extname } from 'path';
import { Connection, Types } from 'mongoose';
import { InjectConnection } from '@nestjs/mongoose';
@@ -23,6 +29,7 @@ import {
UploadedVideoFile,
VideoProcessingService,
} from '../../infrastructure/storage/video-processing.service';
import { MediaProbeService } from '../../infrastructure/storage/media-probe.service';
import { NotificationsService } from '../notifications/notifications.service';
import { AuditService } from '../audit/audit.service';
import { UsersRepository } from '../users/users.repository';
@@ -39,12 +46,7 @@ import { PostsRepository } from './posts.repository';
type PostMediaMetadataInput = Pick<
CreatePostDto,
| 'durationSeconds'
| 'thumbnailUrl'
| 'style'
| 'maqam'
| 'rhythmSignature'
| 'waveformPeaks'
'durationSeconds' | 'thumbnailUrl' | 'style' | 'maqam' | 'rhythmSignature' | 'waveformPeaks'
>;
type NormalizedPostMediaMetadata = {
@@ -61,6 +63,7 @@ type SavedVideoUpload = {
hlsUrl: string;
thumbnailUrl: string;
thumbnailVariants: PostMediaVariantSet | null;
durationSeconds: number | null;
};
type SavedImageUpload = {
@@ -79,6 +82,7 @@ export class PostsService {
private readonly storageService: ManagedStorageService,
private readonly imageProcessingService: ImageProcessingService,
private readonly videoProcessingService: VideoProcessingService,
private readonly mediaProbeService: MediaProbeService,
private readonly feedVersionService: FeedVersionService,
private readonly notificationsService: NotificationsService,
private readonly auditService: AuditService,
@@ -87,7 +91,12 @@ export class PostsService {
async create(
userId: string,
dto: CreatePostDto,
imageFiles: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }> = [],
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 },
@@ -134,6 +143,9 @@ export class PostsService {
const generatedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
const uploadedThumbnailUrl = savedCoverImageUpload?.primaryUrl ?? generatedThumbnailUrl;
const uploadedThumbnailVariants = savedCoverImageUpload?.variants ?? generatedThumbnailVariants;
const uploadedAudioDurationSeconds = audioFile
? await this.mediaProbeService.extractDurationSecondsFromBuffer(audioFile.buffer, audioFile)
: null;
const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
const finalImageUrls = uploadedImageUrls.length ? uploadedImageUrls : inputImageUrls;
const finalImageVariants = uploadedImageVariants.length ? uploadedImageVariants : [];
@@ -141,9 +153,18 @@ export class PostsService {
const finalAudioUrl = uploadedAudioUrl || dto.audioUrl || '';
const finalContent = dto.content?.trim() ?? '';
const taggedUserIds = await this.normalizeTaggedUserIds(dto.taggedUserIds, userId);
const collaboratorIds = await this.normalizeUserIdList(dto.collaboratorIds, userId, 5, 'collaborator');
const collaboratorIds = await this.normalizeUserIdList(
dto.collaboratorIds,
userId,
5,
'collaborator',
);
const imageItems = this.buildImageItems(finalImageUrls, dto.imageCaptions, dto.imageAltTexts);
const mentionResolution = await this.resolveMentionTargets(dto.mentionUsernames, finalContent, userId);
const mentionResolution = await this.resolveMentionTargets(
dto.mentionUsernames,
finalContent,
userId,
);
const { location, latitude, longitude } = this.normalizeLocation(dto);
if (!finalContent && !finalImageUrls.length && !finalVideoUrl && !finalAudioUrl) {
throw new BadRequestException('Post must contain caption or media');
@@ -153,6 +174,7 @@ export class PostsService {
const hashtags = this.extractHashtags(finalContent);
const mediaMetadata = this.normalizeMediaMetadata(dto, postType, undefined, {
audioSourceBuffer: audioFile?.buffer,
extractedDurationSeconds: savedVideoUpload?.durationSeconds ?? uploadedAudioDurationSeconds,
waveformSeed: finalAudioUrl || finalContent || `${userId}:${Date.now()}`,
thumbnailUrl: uploadedThumbnailUrl,
});
@@ -186,7 +208,9 @@ export class PostsService {
await Promise.all([
...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
uploadedHlsUrl ? this.storageService.deleteContainingDirectory(uploadedHlsUrl) : Promise.resolve(),
uploadedHlsUrl
? this.storageService.deleteContainingDirectory(uploadedHlsUrl)
: Promise.resolve(),
uploadedThumbnailUrl
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
: Promise.resolve(),
@@ -204,7 +228,12 @@ export class PostsService {
await this.usersRepository.incrementPostsCount(userId, 1);
await this.feedVersionService.bumpGlobalVersion();
await this.notifyMentionedUsers(userId, post.id, mentionResolution.mentionedUsers, finalContent);
await this.notifyMentionedUsers(
userId,
post.id,
mentionResolution.mentionedUsers,
finalContent,
);
return post;
}
@@ -212,7 +241,12 @@ export class PostsService {
userId: string,
postId: string,
dto: UpdatePostDto,
imageFiles: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }> = [],
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 },
@@ -249,7 +283,10 @@ export class PostsService {
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)) {
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)) {
@@ -272,6 +309,9 @@ export class PostsService {
const generatedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
const uploadedThumbnailUrl = savedCoverImageUpload?.primaryUrl ?? generatedThumbnailUrl;
const uploadedThumbnailVariants = savedCoverImageUpload?.variants ?? generatedThumbnailVariants;
const uploadedAudioDurationSeconds = audioFile
? await this.mediaProbeService.extractDurationSecondsFromBuffer(audioFile.buffer, audioFile)
: null;
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)
@@ -286,7 +326,7 @@ export class PostsService {
? imageFiles.length
? uploadedImageUrls
: inputImageUrls
: post.imageUrls ?? [];
: (post.imageUrls ?? []);
const nextImageVariants = hasImageUpdate
? imageFiles.length
? uploadedImageVariants
@@ -296,34 +336,39 @@ export class PostsService {
const nextVideoUrl = hasVideoUpdate
? videoFile
? uploadedVideoUrl
: dto.videoUrl ?? ''
: post.videoUrl ?? '';
const nextHlsUrl = hasVideoUpdate ? (videoFile ? uploadedHlsUrl : '') : post.hlsUrl ?? '';
: (dto.videoUrl ?? '')
: (post.videoUrl ?? '');
const nextHlsUrl = hasVideoUpdate ? (videoFile ? uploadedHlsUrl : '') : (post.hlsUrl ?? '');
const nextAudioUrl = hasAudioUpdate
? audioFile
? uploadedAudioUrl
: dto.audioUrl ?? ''
: post.audioUrl ?? '';
const nextThumbnailVariants = coverImageFile || videoFile
? uploadedThumbnailVariants
: typeof dto.thumbnailUrl === 'string'
? null
: existingThumbnailVariants;
: (dto.audioUrl ?? '')
: (post.audioUrl ?? '');
const nextThumbnailVariants =
coverImageFile || videoFile
? uploadedThumbnailVariants
: typeof dto.thumbnailUrl === 'string'
? null
: existingThumbnailVariants;
const nextPostType = this.resolvePostType(nextImageUrls, nextVideoUrl, nextAudioUrl);
const nextContent = typeof dto.content === 'string' ? dto.content.trim() : post.content ?? '';
const nextContent = typeof dto.content === 'string' ? dto.content.trim() : (post.content ?? '');
const nextTaggedUserIds =
typeof dto.taggedUserIds !== 'undefined'
? await this.normalizeTaggedUserIds(dto.taggedUserIds, userId)
: (post.taggedUserIds ?? []).map((id: Types.ObjectId | string) => new Types.ObjectId(id.toString()));
: (post.taggedUserIds ?? []).map(
(id: Types.ObjectId | string) => new Types.ObjectId(id.toString()),
);
const nextCollaboratorIds =
typeof dto.collaboratorIds !== 'undefined'
? await this.normalizeUserIdList(dto.collaboratorIds, userId, 5, 'collaborator')
: (post.collaboratorIds ?? []).map((id: Types.ObjectId | string) => new Types.ObjectId(id.toString()));
: (post.collaboratorIds ?? []).map(
(id: Types.ObjectId | string) => new Types.ObjectId(id.toString()),
);
const nextImageItems = this.buildImageItems(
nextImageUrls,
dto.imageCaptions,
dto.imageAltTexts,
hasImageUpdate ? [] : post.imageItems ?? [],
hasImageUpdate ? [] : (post.imageItems ?? []),
);
const previousMentionUsernames = this.normalizeMentionUsernames(post.mentionUsernames ?? []);
const shouldRecomputeMentions =
@@ -334,12 +379,15 @@ export class PostsService {
mentionUsernames: previousMentionUsernames,
mentionedUsers: [] as Array<{ id: string; username: string }>,
};
const { location: nextLocation, latitude: nextLatitude, longitude: nextLongitude } =
this.normalizeLocation(dto, {
location: post.location ?? '',
latitude: post.latitude ?? null,
longitude: post.longitude ?? null,
});
const {
location: nextLocation,
latitude: nextLatitude,
longitude: nextLongitude,
} = this.normalizeLocation(dto, {
location: post.location ?? '',
latitude: post.latitude ?? null,
longitude: post.longitude ?? null,
});
if (!nextContent && !nextImageUrls.length && !nextVideoUrl && !nextAudioUrl) {
throw new BadRequestException('Post must contain caption or media');
}
@@ -356,6 +404,7 @@ export class PostsService {
},
{
audioSourceBuffer: audioFile?.buffer,
extractedDurationSeconds: savedVideoUpload?.durationSeconds ?? uploadedAudioDurationSeconds,
waveformSeed: nextAudioUrl || nextContent || post.id,
thumbnailUrl: uploadedThumbnailUrl,
},
@@ -427,7 +476,9 @@ export class PostsService {
await Promise.all([
...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
uploadedHlsUrl ? this.storageService.deleteContainingDirectory(uploadedHlsUrl) : Promise.resolve(),
uploadedHlsUrl
? this.storageService.deleteContainingDirectory(uploadedHlsUrl)
: Promise.resolve(),
uploadedThumbnailUrl
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
: Promise.resolve(),
@@ -442,7 +493,9 @@ export class PostsService {
await Promise.all([
...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
uploadedHlsUrl ? this.storageService.deleteContainingDirectory(uploadedHlsUrl) : Promise.resolve(),
uploadedHlsUrl
? this.storageService.deleteContainingDirectory(uploadedHlsUrl)
: Promise.resolve(),
uploadedThumbnailUrl
? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
: Promise.resolve(),
@@ -471,7 +524,10 @@ export class PostsService {
}
if (hasImageUpdate) {
const nextImageSet = new Set(updated.imageUrls ?? []);
const existingImageAssets = this.buildSavedImageAssets(post.imageUrls ?? [], existingImageVariants);
const existingImageAssets = this.buildSavedImageAssets(
post.imageUrls ?? [],
existingImageVariants,
);
await Promise.all(
existingImageAssets
.filter((asset) => !nextImageSet.has(asset.primaryUrl))
@@ -692,7 +748,10 @@ export class PostsService {
});
}
async registerView(userId: string, postId: string): Promise<{ success: true; postId: string; viewCount: number }> {
async registerView(
userId: string,
postId: string,
): Promise<{ success: true; postId: string; viewCount: number }> {
const post = await this.postsRepository.findById(postId);
if (!post) {
throw new NotFoundException('Post not found');
@@ -771,14 +830,18 @@ export class PostsService {
): Promise<PostDocument> {
await this.assertPostOwner(userId, postId);
const updated = await this.postsRepository.updateById(postId, {
...(typeof dto.commentsDisabled === 'boolean' ? { commentsDisabled: dto.commentsDisabled } : {}),
...(typeof dto.commentsDisabled === 'boolean'
? { commentsDisabled: dto.commentsDisabled }
: {}),
...(typeof dto.commentsFollowersOnly === 'boolean'
? { commentsFollowersOnly: dto.commentsFollowersOnly }
: {}),
...(Array.isArray(dto.commentFilterKeywords)
? {
commentFilterKeywords: Array.from(
new Set(dto.commentFilterKeywords.map((item) => item.trim().toLowerCase()).filter(Boolean)),
new Set(
dto.commentFilterKeywords.map((item) => item.trim().toLowerCase()).filter(Boolean),
),
).slice(0, 50),
}
: {}),
@@ -809,7 +872,10 @@ export class PostsService {
async archive(userId: string, postId: string): Promise<PostDocument> {
await this.assertPostOwner(userId, postId);
const updated = await this.postsRepository.updateById(postId, { isArchived: true, pinnedToProfile: false });
const updated = await this.postsRepository.updateById(postId, {
isArchived: true,
pinnedToProfile: false,
});
if (!updated) {
throw new NotFoundException('Post not found');
}
@@ -896,7 +962,11 @@ export class PostsService {
return updated;
}
private resolvePostType(imageUrls: string[] = [], videoUrl?: string, audioUrl?: string): PostType {
private resolvePostType(
imageUrls: string[] = [],
videoUrl?: string,
audioUrl?: string,
): PostType {
const hasImages = imageUrls.length > 0;
const hasVideo = !!videoUrl?.trim();
const hasAudio = !!audioUrl?.trim();
@@ -933,6 +1003,7 @@ export class PostsService {
},
options: {
audioSourceBuffer?: Buffer;
extractedDurationSeconds?: number | null;
waveformSeed?: string;
thumbnailUrl?: string;
} = {},
@@ -942,7 +1013,9 @@ export class PostsService {
if (!supportsMediaMetadata) {
if (this.hasMediaMetadataInput(dto)) {
throw new BadRequestException('Audio/video metadata is allowed only for audio or video posts');
throw new BadRequestException(
'Audio/video metadata is allowed only for audio or video posts',
);
}
return {
durationSeconds: null,
@@ -960,7 +1033,11 @@ export class PostsService {
return {
durationSeconds:
typeof dto.durationSeconds === 'number' ? dto.durationSeconds : fallback.durationSeconds,
typeof options.extractedDurationSeconds === 'number'
? options.extractedDurationSeconds
: typeof dto.durationSeconds === 'number'
? dto.durationSeconds
: fallback.durationSeconds,
thumbnailUrl:
typeof dto.thumbnailUrl === 'string'
? dto.thumbnailUrl.trim()
@@ -1036,7 +1113,10 @@ export class PostsService {
const users = await this.usersRepository.findByUsernames(mergedMentionUsernames);
const userByUsername = new Map(
users.map((user) => [user.username.toLowerCase(), { id: user.id, username: user.username.toLowerCase() }]),
users.map((user) => [
user.username.toLowerCase(),
{ id: user.id, username: user.username.toLowerCase() },
]),
);
const mentionedUsers = mergedMentionUsernames
@@ -1088,8 +1168,14 @@ export class PostsService {
): PostImageItem[] {
return imageUrls.map((url, index) => ({
url,
caption: typeof captions?.[index] === 'string' ? captions[index].trim() : fallback[index]?.caption ?? '',
altText: typeof altTexts?.[index] === 'string' ? altTexts[index].trim() : fallback[index]?.altText ?? '',
caption:
typeof captions?.[index] === 'string'
? captions[index].trim()
: (fallback[index]?.caption ?? ''),
altText:
typeof altTexts?.[index] === 'string'
? altTexts[index].trim()
: (fallback[index]?.altText ?? ''),
order: index,
}));
}
@@ -1187,10 +1273,15 @@ export class PostsService {
await Promise.all(
mentionedUsers.map(async (mentionedUser) => {
try {
await this.notificationsService.createMentionNotification(actorId, mentionedUser.id, postId, {
resourceType: 'post',
previewText: content.slice(0, 140),
});
await this.notificationsService.createMentionNotification(
actorId,
mentionedUser.id,
postId,
{
resourceType: 'post',
previewText: content.slice(0, 140),
},
);
} catch (error) {
this.logger.warn(
`Mention notification failed for actor=${actorId} recipient=${mentionedUser.id}: ${
@@ -1206,6 +1297,10 @@ export class PostsService {
this.validateMediaFile('video', file);
const optimized = await this.videoProcessingService.optimizeForPlayback(file);
const extension = this.validateMediaFile('video', optimized.file);
const durationSeconds = await this.mediaProbeService.extractDurationSecondsFromBuffer(
optimized.file.buffer,
optimized.file,
);
let videoUrl = '';
let hlsUrl = '';
@@ -1241,18 +1336,24 @@ export class PostsService {
thumbnailVariants = savedThumbnail.variants;
}
return { videoUrl, hlsUrl, thumbnailUrl, thumbnailVariants };
return { videoUrl, hlsUrl, thumbnailUrl, thumbnailVariants, durationSeconds };
} catch (error) {
await Promise.all([
videoUrl ? this.deleteManagedPostMedia(videoUrl) : Promise.resolve(),
hlsUrl ? this.storageService.deleteContainingDirectory(hlsUrl) : Promise.resolve(),
thumbnailUrl ? this.deleteThumbnailAsset(thumbnailUrl, thumbnailVariants) : Promise.resolve(),
thumbnailUrl
? this.deleteThumbnailAsset(thumbnailUrl, thumbnailVariants)
: Promise.resolve(),
]);
throw error;
}
}
async createRepost(userId: string, sourcePostId: string, dto: CreateRepostDto): Promise<PostDocument> {
async createRepost(
userId: string,
sourcePostId: string,
dto: CreateRepostDto,
): Promise<PostDocument> {
const sourcePost = await this.postsRepository.findById(sourcePostId);
if (!sourcePost) {
throw new NotFoundException('Source post not found');
@@ -1289,8 +1390,7 @@ export class PostsService {
file: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
): Promise<string> {
const extension = this.validateMediaFile(mediaType, file);
const folder =
mediaType === 'image' ? 'images' : mediaType === 'video' ? 'videos' : 'audios';
const folder = mediaType === 'image' ? 'images' : mediaType === 'video' ? 'videos' : 'audios';
return this.storageService.saveFile({
folderSegments: ['posts', folder],
extension,
@@ -1310,8 +1410,8 @@ export class PostsService {
mediaType === 'image'
? 'imageFiles must be jpg, jpeg, png, webp, or gif'
: mediaType === 'video'
? 'videoFile must be mp4, mov, webm, mkv, or avi'
: 'audioFile must be mp3, wav, m4a, aac, ogg, or webm',
? 'videoFile must be mp4, mov, webm, mkv, or avi'
: 'audioFile must be mp3, wav, m4a, aac, ogg, or webm',
);
}
@@ -1326,8 +1426,8 @@ export class PostsService {
mediaType === 'image'
? 'Each image must be 10MB or less'
: mediaType === 'video'
? 'videoFile size must be 100MB or less'
: 'audioFile size must be 20MB or less',
? 'videoFile size must be 100MB or less'
: 'audioFile size must be 20MB or less',
);
}
@@ -1539,7 +1639,8 @@ export class PostsService {
thumbnailVariants?.originalUrl ||
'';
const hasManagedVariantGroup = !!thumbnailVariants &&
const hasManagedVariantGroup =
!!thumbnailVariants &&
[
thumbnailVariants.originalUrl,
thumbnailVariants.lowUrl,