Skip to content

Commit

Permalink
Update media-converter extension (#15819)
Browse files Browse the repository at this point in the history
- add heic support and fix bugs
- Initial commit

Co-authored-by: Leandro Maia <leandro.llfm@outlook.com>
  • Loading branch information
llfmaia and Leandro Maia authored Dec 13, 2024
1 parent 50b9e92 commit 5eb4958
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 69 deletions.
5 changes: 5 additions & 0 deletions extensions/media-converter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
47 changes: 8 additions & 39 deletions extensions/media-converter/src/components/ConverterForm.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);

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);
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -162,13 +136,7 @@ export function ConverterForm() {
</ActionPanel>
}
>
<Form.FilePicker
id="videoFile"
title="Select files"
allowMultipleSelection={true}
value={finderFiles}
onChange={handleFileSelect}
/>
<Form.FilePicker id="videoFile" title="Select files" allowMultipleSelection={true} onChange={handleFileSelect} />
{selectedFileType && (
<Form.Dropdown
id="format"
Expand All @@ -180,6 +148,7 @@ export function ConverterForm() {
<Form.Dropdown.Item value="jpg" title=".jpg" />
<Form.Dropdown.Item value="png" title=".png" />
<Form.Dropdown.Item value="webp" title=".webp" />
<Form.Dropdown.Item value="heic" title=".heic" />
</Form.Dropdown.Section>
) : selectedFileType === "audio" ? (
<Form.Dropdown.Section title="Audio Formats">
Expand Down
6 changes: 3 additions & 3 deletions extensions/media-converter/src/quick-convert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
};

Expand Down Expand Up @@ -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");
Expand Down
53 changes: 26 additions & 27 deletions extensions/media-converter/src/utils/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -46,6 +47,9 @@ const imageConfig = {
fileExtension: ".webp",
quality: "100",
},
heic: {
fileExtension: ".heic",
},
};

const audioConfig = {
Expand All @@ -67,22 +71,28 @@ const audioConfig = {
},
};

export async function convertVideo(
filePath: string,
outputFormat: "mp4" | "avi" | "mkv" | "mov" | "mpg",
): Promise<string> {
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<string> {
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}"`;

Expand All @@ -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<string> {
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;

Expand All @@ -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<string> {
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}"`;
Expand Down

0 comments on commit 5eb4958

Please sign in to comment.