From 5eb49588f61370342db51b69b02dd00ea10dca13 Mon Sep 17 00:00:00 2001 From: Leandro Lima <89325878+llfmaia@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:33:12 -0300 Subject: [PATCH] Update media-converter extension (#15819) - add heic support and fix bugs - Initial commit Co-authored-by: Leandro Maia --- extensions/media-converter/CHANGELOG.md | 5 ++ .../src/components/ConverterForm.tsx | 47 +++------------- .../media-converter/src/quick-convert.tsx | 6 +-- .../media-converter/src/utils/converter.ts | 53 +++++++++---------- 4 files changed, 42 insertions(+), 69 deletions(-) diff --git a/extensions/media-converter/CHANGELOG.md b/extensions/media-converter/CHANGELOG.md index cf6fcb70155e3..7ecebe4e5b22d 100644 --- a/extensions/media-converter/CHANGELOG.md +++ b/extensions/media-converter/CHANGELOG.md @@ -1,5 +1,10 @@ # Media Converter Changelog +## [Enhancement] - 2024-12-11 + +- Added support for HEIC file format conversion using the sips command. +- Fixed a bug where the Convert Media command would not work as expected. + ## [Enhancement] - 2024-12-10 - Added a new **Quick Convert** command that allows users to select a file in Finder, choose the desired format from a list, and convert it instantly. diff --git a/extensions/media-converter/src/components/ConverterForm.tsx b/extensions/media-converter/src/components/ConverterForm.tsx index 850a2f53ffb3a..6404a28c97002 100644 --- a/extensions/media-converter/src/components/ConverterForm.tsx +++ b/extensions/media-converter/src/components/ConverterForm.tsx @@ -1,41 +1,15 @@ -import { Form, ActionPanel, Action, showToast, Toast, getSelectedFinderItems } from "@raycast/api"; -import { useState, useEffect } from "react"; +import { Form, ActionPanel, Action, showToast, Toast } from "@raycast/api"; +import { useState } from "react"; import path from "path"; import { convertVideo, convertImage, convertAudio } from "../utils/converter"; import { execPromise } from "../utils/exec"; -const ALLOWED_EXTENSIONS = [".mov", ".mp4", ".avi", ".mkv", ".mpg"]; -const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".png", ".webp"]; +const ALLOWED_EXTENSIONS = [".mov", ".mp4", ".avi", ".mkv", ".mpg", ".heic"]; +const ALLOWED_IMAGE_EXTENSIONS = [".jpg", ".png", ".webp", ".heic"]; const ALLOWED_AUDIO_EXTENSIONS = [".mp3", ".aac", ".wav", ".m4a", ".flac"]; export function ConverterForm() { const [selectedFileType, setSelectedFileType] = useState<"video" | "image" | "audio" | null>(null); - const [finderFiles, setFinderFiles] = useState([]); - - useEffect(() => { - const fetchFinderItems = async () => { - try { - const items = await getSelectedFinderItems(); - const validFiles = items - .filter((item) => { - const ext = path.extname(item.path).toLowerCase(); - return ( - ALLOWED_EXTENSIONS.includes(ext) || - ALLOWED_IMAGE_EXTENSIONS.includes(ext) || - ALLOWED_AUDIO_EXTENSIONS.includes(ext) - ); - }) - .map((item) => item.path); - - setFinderFiles(validFiles); - if (validFiles.length > 0) handleFileSelect(validFiles); - } catch (error) { - console.error("Error fetching Finder items:", error); - } - }; - fetchFinderItems(); - }, []); - const handleFileSelect = (files: string[]) => { if (files.length === 0) { setSelectedFileType(null); @@ -94,7 +68,7 @@ export function ConverterForm() { const isInputImage = ALLOWED_IMAGE_EXTENSIONS.includes(fileExtension); const isInputAudio = ALLOWED_AUDIO_EXTENSIONS.includes(fileExtension); const isOutputVideo = ["mp4", "avi", "mkv", "mov", "mpg"].includes(values.format); - const isOutputImage = ["jpg", "png", "webp"].includes(values.format); + const isOutputImage = ["jpg", "png", "webp", "heic"].includes(values.format); if (!isInputVideo && !isInputImage && !isInputAudio) { await showToast({ @@ -123,7 +97,7 @@ export function ConverterForm() { try { let outputPath = ""; if (isInputImage) { - outputPath = await convertImage(item, values.format as "jpg" | "png" | "webp"); + outputPath = await convertImage(item, values.format as "jpg" | "png" | "webp" | "heic"); } else if (isInputAudio) { outputPath = await convertAudio(item, values.format as "mp3" | "aac" | "wav" | "flac"); } else { @@ -162,13 +136,7 @@ export function ConverterForm() { } > - + {selectedFileType && ( + ) : selectedFileType === "audio" ? ( diff --git a/extensions/media-converter/src/quick-convert.tsx b/extensions/media-converter/src/quick-convert.tsx index 17d766c171b8b..fb10ec96cf30d 100644 --- a/extensions/media-converter/src/quick-convert.tsx +++ b/extensions/media-converter/src/quick-convert.tsx @@ -7,14 +7,14 @@ import { execPromise } from "./utils/exec"; // Comprehensive list of allowed file extensions const FILE_TYPE_EXTENSIONS = { video: [".mov", ".mp4", ".avi", ".mkv", ".mpg", ".m4v"], - image: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff"], + image: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".heic"], audio: [".mp3", ".aac", ".wav", ".m4a", ".flac", ".ogg", ".wma"], }; // Mapping of file types to conversion formats const CONVERSION_FORMATS = { video: ["mp4", "avi", "mkv", "mov", "mpg"], - image: ["jpg", "png", "webp"], + image: ["jpg", "png", "webp", "heic"], audio: ["mp3", "aac", "wav", "flac"], }; @@ -104,7 +104,7 @@ export default function QuickConvert() { let outputPath = ""; switch (fileType) { case "image": - outputPath = await convertImage(selectedFile, format as "jpg" | "png" | "webp"); + outputPath = await convertImage(selectedFile, format as "jpg" | "png" | "webp" | "heic"); break; case "audio": outputPath = await convertAudio(selectedFile, format as "mp3" | "aac" | "wav" | "flac"); diff --git a/extensions/media-converter/src/utils/converter.ts b/extensions/media-converter/src/utils/converter.ts index 568d79eeae1f0..d4ec19ef9e587 100644 --- a/extensions/media-converter/src/utils/converter.ts +++ b/extensions/media-converter/src/utils/converter.ts @@ -2,6 +2,7 @@ import path from "path"; import fs from "fs"; import { getFFmpegPath } from "./ffmpeg"; import { execPromise } from "./exec"; +import { execSync } from "child_process"; const config = { ffmpegOptions: { @@ -46,6 +47,9 @@ const imageConfig = { fileExtension: ".webp", quality: "100", }, + heic: { + fileExtension: ".heic", + }, }; const audioConfig = { @@ -67,22 +71,28 @@ const audioConfig = { }, }; -export async function convertVideo( - filePath: string, - outputFormat: "mp4" | "avi" | "mkv" | "mov" | "mpg", -): Promise { - const formatOptions = config.ffmpegOptions[outputFormat]; - const outputFilePath = filePath.replace(path.extname(filePath), formatOptions.fileExtension); +function getUniqueOutputPath(filePath: string, extension: string): string { + const outputFilePath = filePath.replace(path.extname(filePath), extension); let finalOutputPath = outputFilePath; let counter = 1; while (fs.existsSync(finalOutputPath)) { - const fileName = path.basename(outputFilePath, formatOptions.fileExtension); + const fileName = path.basename(outputFilePath, extension); const dirName = path.dirname(outputFilePath); - finalOutputPath = path.join(dirName, `${fileName}(${counter})${formatOptions.fileExtension}`); + finalOutputPath = path.join(dirName, `${fileName}(${counter})${extension}`); counter++; } + return finalOutputPath; +} + +export async function convertVideo( + filePath: string, + outputFormat: "mp4" | "avi" | "mkv" | "mov" | "mpg", +): Promise { + const formatOptions = config.ffmpegOptions[outputFormat]; + const finalOutputPath = getUniqueOutputPath(filePath, formatOptions.fileExtension); + const ffmpegPath = await getFFmpegPath(); const command = `"${ffmpegPath}" -i "${filePath}" -vcodec ${formatOptions.videoCodec} -acodec ${formatOptions.audioCodec} "${finalOutputPath}"`; @@ -91,19 +101,17 @@ export async function convertVideo( return finalOutputPath; } -export async function convertImage(filePath: string, outputFormat: "jpg" | "png" | "webp") { +export async function convertImage(filePath: string, outputFormat: "jpg" | "png" | "webp" | "heic"): Promise { const formatOptions = imageConfig[outputFormat]; - const outputFilePath = filePath.replace(path.extname(filePath), formatOptions.fileExtension); - let finalOutputPath = outputFilePath; - let counter = 1; + const finalOutputPath = getUniqueOutputPath(filePath, formatOptions.fileExtension); - while (fs.existsSync(finalOutputPath)) { - const fileName = path.basename(outputFilePath, formatOptions.fileExtension); - const dirName = path.dirname(outputFilePath); - finalOutputPath = path.join(dirName, `${fileName}(${counter})${formatOptions.fileExtension}`); - counter++; + if (outputFormat === "heic") { + // Use sips for HEIC conversion + execSync(`sips --setProperty format heic "${filePath}" --out "${finalOutputPath}"`); + return finalOutputPath; } + // Use FFmpeg for other image formats const ffmpegPath = await getFFmpegPath(); let command; @@ -128,16 +136,7 @@ export async function convertImage(filePath: string, outputFormat: "jpg" | "png" export async function convertAudio(filePath: string, outputFormat: "mp3" | "aac" | "wav" | "flac"): Promise { const formatOptions = audioConfig[outputFormat]; - const outputFilePath = filePath.replace(path.extname(filePath), formatOptions.fileExtension); - let finalOutputPath = outputFilePath; - let counter = 1; - - while (fs.existsSync(finalOutputPath)) { - const fileName = path.basename(outputFilePath, formatOptions.fileExtension); - const dirName = path.dirname(outputFilePath); - finalOutputPath = path.join(dirName, `${fileName}(${counter})${formatOptions.fileExtension}`); - counter++; - } + const finalOutputPath = getUniqueOutputPath(filePath, formatOptions.fileExtension); const ffmpegPath = await getFFmpegPath(); const command = `"${ffmpegPath}" -i "${filePath}" -c:a ${formatOptions.audioCodec} "${finalOutputPath}"`;