import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" import { FfprobeData } from "@/features/media/types/ffprobe" import { MediaFile } from "@/features/media/types/media" /** * Объединяет классы CSS с помощью clsx и tailwind-merge * * @param inputs - Массив классов CSS для объединения * @returns Строка с объединенными классами CSS * * @example * ```tsx *
* Текст с красным цветом и, возможно, синим фоном *
* ``` */ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } /** * Форматирует разрешение видео в стандартное обозначение (FHD, UHD, 4K и т.д.) * * @param width - Ширина видео в пикселях * @param height - Высота видео в пикселях * @returns Строка с обозначением разрешения (FHD, UHD, 4K и т.д.) * * @example * ```ts * formatResolution(1920, 1080) // "FHD" * formatResolution(3840, 2160) // "UHD" * ``` */ export function formatResolution(width: number, height: number): string { const pixels = width * height if (height > width) { ;[width, height] = [height, width] } // console.log(`[formatResolution] width: ${width}, height: ${height}, pixels: ${pixels}`) // 1920×1080 (FHD) if (width === 1920 && height === 1080) return "FHD" // 2048×1080 (DCI 2K) if (width === 2048 && height === 1080) return "DCI 2K" // 2560×1440 (QHD) if (width === 2560 && height === 1440) return "QHD" // 3200×1800 (QHD+) if (width === 3200 && height === 1800) return "QHD+" // 3840×2160 (4K UHD) if (width === 3840 && height === 2160) return "UHD" // 4096×2160 (DCI 4K) if (width === 4096 && height === 2160) return "DCI 4K" // 5120×2880 = 14,745,600 pixels if (width === 5120 && height === 2880) return "5K" // 6144×3240 = 19,906,560 pixels if (width === 6144 && height === 3240) return "6K" // 7680×4320 = 33,177,600 pixels if (width === 7680 && height === 4320) return "8K UHD" // 7680×3264 = 25,280,000 pixels if (pixels >= 24576000) return "8K UHD" // 7680x3264 if (pixels >= 19906560) return "6K" // 6144x3240 // 5120×2880 = 14,745,600 pixels if (pixels >= 14745600) return "5K" if (pixels >= 8294400) return "UHD" // 3840x2160 if (pixels >= 6048000) return "QHD+" // 3240x1800 if (pixels >= 4147200) return "QHD" // 2560x1440 if (pixels >= 2073600) return "FHD" // 1920x1080 if (pixels >= 921600) return "HD" // 1280x720 return "SD" } /** * Форматирует битрейт видео в Mbps * * @param bitrate - Битрейт видео в битах в секунду * @returns Строка с битрейтом в Mbps * * @example * ```ts * formatBitrate(5000000) // "5.0 Mbps" * formatBitrate(undefined) // "N/A" * ``` */ export function formatBitrate(bitrate: number | undefined): string { if (!bitrate) return "N/A" return `${(bitrate / 1_000_000).toFixed(1)} Mbps` } /** * Генерирует уникальный идентификатор для нового видео * * @param videos - Массив существующих видео * @returns Строка с уникальным идентификатором для нового видео в формате "V{n}" * * @example * ```ts * // Если в массиве есть видео с id "V1", "V2", "V3" * generateVideoId(videos) // "V4" * ``` */ export function generateVideoId(videos: MediaFile[]): string { // Сортируем видео по дате создания const sortedVideos = [...videos].sort((a, b) => { const timeA = new Date(a.startTime ?? 0).getTime() const timeB = new Date(b.startTime ?? 0).getTime() return timeA - timeB }) // Находим максимальный существующий номер const maxNumber = sortedVideos.reduce((max, video) => { const match = /V(\d+)/.exec(video.id) if (match) { const num = Number.parseInt(match[1]) return num > max ? num : max } return max }, 0) return `V${maxNumber + 1}` } /** * Проверяет, доступно ли видео в указанное время * * @param video - Объект видео * @param currentTime - Текущее время в секундах * @param tolerance - Допустимое отклонение в секундах (по умолчанию 0.3) * @returns true, если видео доступно в указанное время, иначе false * * @example * ```ts * // Видео с началом в 10 секунд и длительностью 5 секунд * isVideoAvailable(video, 12) // true * isVideoAvailable(video, 20) // false * ``` */ export function isVideoAvailable(video: MediaFile, currentTime: number, tolerance = 0.3): boolean { const startTime = video.startTime ?? 0 const endTime = startTime + (video.duration ?? 0) return currentTime >= startTime - tolerance && currentTime <= endTime + tolerance } /** * Извлекает дату и время из имени файла в формате "YYYYMMDD_HHMMSS" * * @param fileName - Имя файла * @returns Объект Date, если удалось извлечь дату и время, иначе null * * @example * ```ts * parseFileNameDateTime("IMG_20240910_170942.jpg") // Date object for 2024-09-10 17:09:42 * parseFileNameDateTime("image.jpg") // null * ``` */ export function parseFileNameDateTime(fileName: string): Date | null { const match = /\d{8}_\d{6}/.exec(fileName) if (!match) return null const dateTimeStr = match[0] // "20240910_170942" const year = dateTimeStr.slice(0, 4) const month = dateTimeStr.slice(4, 6) const day = dateTimeStr.slice(6, 8) const hour = dateTimeStr.slice(9, 11) const minute = dateTimeStr.slice(11, 13) const second = dateTimeStr.slice(13, 15) return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`) } /** * Форматирует размер файла в человекочитаемый формат * * @param bytes - Размер файла в байтах * @returns Строка с размером файла в человекочитаемом формате (B, KB, MB, GB, TB) * * @example * ```ts * formatFileSize(1024) // "1.0 KB" * formatFileSize(1048576) // "1.0 MB" * ``` */ export function formatFileSize(bytes: number): string { const units = ["B", "KB", "MB", "GB", "TB"] let size = bytes let unitIndex = 0 while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024 unitIndex++ } return `${size.toFixed(1)} ${units[unitIndex]}` } /** * Определяет время создания медиафайла из данных ffprobe * * @param probeData - Данные ffprobe о медиафайле * @returns Время создания медиафайла в секундах (UNIX timestamp) * * @description * Функция пытается определить время создания медиафайла в следующем порядке: * 1. Из метаданных (creation_time) * 2. Из имени файла (если оно содержит дату и время в формате "YYYYMMDD_HHMMSS") * 3. Из start_time в данных ffprobe * 4. Если все вышеперечисленные методы не сработали, возвращает текущее время */ export function getMediaCreationTime(probeData: FfprobeData): number { // 1. Try to get from probeData metadata if (probeData.format.tags?.creation_time) { const time = new Date(probeData.format.tags.creation_time).getTime() / 1000 // console.log( // `[getMediaCreationTime] Время из метаданных: ${new Date(time * 1000).toISOString()}`, // ) return time } // 2. Try to parse from filename (e.g. "20240910_170942") const parsedDate = probeData.format.filename ? parseFileNameDateTime(probeData.format.filename) : null if (parsedDate) { const time = parsedDate.getTime() / 1000 // console.log( // `[getMediaCreationTime] Время из имени файла: ${new Date(time * 1000).toISOString()}`, // ) return time } // 3. Try to get from probeData start_time const startTime = probeData.format.start_time if (startTime) { console.log(`[getMediaCreationTime] Время из start_time: ${new Date(startTime * 1000).toISOString()}`) return startTime } // 4. If all else fails, return current time console.warn( `[getMediaCreationTime] Не удалось определить время создания файла ${probeData.format.filename}, используем текущее время`, ) return Math.floor(Date.now() / 1000) } /** * Генерирует уникальный идентификатор * * @param prefix - Префикс для идентификатора (необязательно) * @returns Строка с уникальным идентификатором * * @example * ```ts * generateId() // "abc123def456" * generateId("clip") // "clip_abc123def456" * ``` */ export function generateId(prefix?: string): string { const id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) return prefix ? `${prefix}_${id}` : id }