import { useEffect, useMemo, useState } from "react"; import { CheckCircle2, FileAudio, FolderOpen, Gauge, Music2 } from "lucide-react"; import { Button } from "../components/Button"; import { FileDropzone } from "../components/FileDropzone"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../components/Select"; import { convertAudioFileToMp3 } from "../helpers/audioProcessing"; import { downloadBlob, sanitizeFilename, saveBlobToDirectory, type WritableDirectoryHandle, } from "../helpers/mp3Tags"; import { ensureDirectoryWritePermission, loadOutputDirectory, saveOutputDirectory, } from "../helpers/directoryStore"; import { loadSpotiRipSettings } from "../helpers/spotiripSettings"; import { addLibraryHistory } from "../helpers/libraryHistory"; import styles from "./convert.module.css"; import { MobileNav } from "../components/MobileNav"; type ConvertItem = { id: string; file: File; status: "ready" | "converting" | "saved" | "error"; output?: string; error?: string; }; export default function ConvertPage() { const [settings] = useState(() => loadSpotiRipSettings()); const [items, setItems] = useState([]); const [bitrate, setBitrate] = useState(settings.mp3Bitrate); const [directory, setDirectory] = useState(); const [working, setWorking] = useState(false); const [message, setMessage] = useState(); useEffect(() => { loadOutputDirectory().then(setDirectory).catch(() => undefined); }, []); const readyCount = useMemo(() => items.filter((item) => item.status === "ready" || item.status === "error").length, [items]); function addFiles(files: File[]) { setItems((current) => [ ...current, ...files .filter((file) => !current.some((item) => item.id === `${file.name}-${file.size}-${file.lastModified}`)) .map((file) => ({ id: `${file.name}-${file.size}-${file.lastModified}`, file, status: "ready" as const, })), ]); } async function chooseFolder() { const picker = (window as typeof window & { showDirectoryPicker?: () => Promise; }).showDirectoryPicker; if (!picker) { setMessage("Direct folder writing is unavailable in this browser; normal downloads will be used."); return; } try { const selected = await picker(); setDirectory(selected); await saveOutputDirectory(selected); setMessage(`Output folder: ${selected.name}`); } catch { } } async function convertAll() { const pending = items.filter((item) => item.status === "ready" || item.status === "error"); if (!pending.length || working) return; setWorking(true); setMessage(undefined); let activeDirectory = directory; if (activeDirectory && !(await ensureDirectoryWritePermission(activeDirectory))) { activeDirectory = undefined; setDirectory(undefined); setMessage("Folder permission was not granted; converted files will download normally."); } let completed = 0; for (const item of pending) { setItems((current) => current.map((entry) => entry.id === item.id ? { ...entry, status: "converting", error: undefined } : entry)); try { const mp3 = await convertAudioFileToMp3(item.file, bitrate); const base = sanitizeFilename(item.file.name.replace(/\.[^.]+$/, "")) || "Converted Audio"; const filename = base + ".mp3"; const output = activeDirectory ? await saveBlobToDirectory(activeDirectory, filename, mp3) : (downloadBlob(filename, mp3), filename); setItems((current) => current.map((entry) => entry.id === item.id ? { ...entry, status: "saved", output } : entry)); completed += 1; } catch (reason) { const detail = reason instanceof Error ? reason.message : "Conversion failed."; setItems((current) => current.map((entry) => entry.id === item.id ? { ...entry, status: "error", error: detail } : entry)); } } if (completed > 0) { addLibraryHistory({ kind: "convert", title: `Converted ${completed} file${completed === 1 ? "" : "s"} to MP3`, trackCount: completed, }); setMessage( activeDirectory ? `Converted ${completed} file${completed === 1 ? "" : "s"} to “${activeDirectory.name}”.` : `Converted ${completed} file${completed === 1 ? "" : "s"} and prepared browser downloads.`, ); } setWorking(false); } return (
Local Converter
LOCAL AUDIO TOOLS

Convert to MP3

Convert audio files on your device without uploading the full source file to SpotiRip.

{message ?
{message}
: null}
} title="Drop audio files here" subtitle="WAV, M4A, AAC, OGG or MP3 · conversion happens on this device" />
{items.length} file{items.length === 1 ? "" : "s"} · {readyCount} waiting
{items.map((item, index) => (
{String(index + 1).padStart(2, "0")}
{item.file.name} {(item.file.size / 1024 / 1024).toFixed(1)} MB
{item.status === "saved" ? <> {item.output} : item.status === "converting" ? "Converting…" : item.status === "error" ? item.error : "Ready"}
))} {!items.length ?
Add audio files above to create an MP3 conversion queue.
: null}
); }