diff --git a/app.go b/app.go
index c6f8fdd..e3b355c 100644
--- a/app.go
+++ b/app.go
@@ -684,6 +684,11 @@ func (a *App) SelectAudioFiles() ([]string, error) {
return files, nil
}
+// GetFileSizes returns file sizes for a list of file paths
+func (a *App) GetFileSizes(files []string) map[string]int64 {
+ return backend.GetFileSizes(files)
+}
+
// ListDirectoryFiles lists files and folders in a directory
func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) {
if dirPath == "" {
diff --git a/backend/analysis.go b/backend/analysis.go
index f551ae5..0ca4214 100644
--- a/backend/analysis.go
+++ b/backend/analysis.go
@@ -12,6 +12,7 @@ import (
// AnalysisResult contains the audio analysis data
type AnalysisResult struct {
FilePath string `json:"file_path"`
+ FileSize int64 `json:"file_size"`
SampleRate uint32 `json:"sample_rate"`
Channels uint8 `json:"channels"`
BitsPerSample uint8 `json:"bits_per_sample"`
@@ -30,6 +31,12 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
return nil, fmt.Errorf("file does not exist: %s", filepath)
}
+ // Get file size
+ fileInfo, err := os.Stat(filepath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get file info: %w", err)
+ }
+
// Parse FLAC file
f, err := flac.ParseFile(filepath)
if err != nil {
@@ -38,6 +45,7 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
result := &AnalysisResult{
FilePath: filepath,
+ FileSize: fileInfo.Size(),
}
// Extract basic audio properties from STREAMINFO block
diff --git a/backend/filemanager.go b/backend/filemanager.go
index 8cb6242..be1a754 100644
--- a/backend/filemanager.go
+++ b/backend/filemanager.go
@@ -344,6 +344,18 @@ func PreviewRename(files []string, format string) []RenamePreview {
return previews
}
+// GetFileSizes returns file sizes for a list of file paths
+func GetFileSizes(files []string) map[string]int64 {
+ result := make(map[string]int64)
+ for _, filePath := range files {
+ info, err := os.Stat(filePath)
+ if err == nil {
+ result[filePath] = info.Size()
+ }
+ }
+ return result
+}
+
// RenameFiles renames files based on their metadata
func RenameFiles(files []string, format string) []RenameResult {
var results []RenameResult
diff --git a/frontend/src/components/AudioAnalysis.tsx b/frontend/src/components/AudioAnalysis.tsx
index 18b87e4..589bd35 100644
--- a/frontend/src/components/AudioAnalysis.tsx
+++ b/frontend/src/components/AudioAnalysis.tsx
@@ -8,7 +8,8 @@ import {
TrendingUp,
FileAudio,
Clock,
- Gauge
+ Gauge,
+ HardDrive
} from "lucide-react";
import type { AnalysisResult } from "@/types/api";
@@ -78,6 +79,14 @@ export function AudioAnalysis({
return num.toFixed(2);
};
+ const formatFileSize = (bytes: number): string => {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
+ };
+
// Calculate Nyquist frequency (half of sample rate)
const nyquistFreq = result.sample_rate / 2;
@@ -117,6 +126,13 @@ export function AudioAnalysis({
Nyquist:
{(nyquistFreq / 1000).toFixed(1)} kHz
+ {result.file_size > 0 && (
+
+
+ Size:
+ {formatFileSize(result.file_size)}
+
+ )}
{/* Dynamic Range - Single line */}
diff --git a/frontend/src/components/AudioConverterPage.tsx b/frontend/src/components/AudioConverterPage.tsx
index 8353752..b42020e 100644
--- a/frontend/src/components/AudioConverterPage.tsx
+++ b/frontend/src/components/AudioConverterPage.tsx
@@ -30,11 +30,20 @@ interface AudioFile {
path: string;
name: string;
format: string;
+ size: number;
status: "pending" | "converting" | "success" | "error";
error?: string;
outputPath?: string;
}
+function formatFileSize(bytes: number): string {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
+}
+
const BITRATE_OPTIONS = [
{ value: "320k", label: "320k" },
{ value: "256k", label: "256k" },
@@ -141,10 +150,19 @@ export function AudioConverterPage() {
if (allMP3 && outputFormat !== "m4a") {
setOutputFormat("m4a");
}
- }, [files, outputFormat]);
+
+ // Reset to AAC if no FLAC files (ALAC doesn't make sense for lossy input)
+ const hasFlac = files.some((f) => f.format === "flac");
+ if (!hasFlac && m4aCodec === "alac") {
+ setM4aCodec("aac");
+ }
+ }, [files, outputFormat, m4aCodec]);
// Check if format selection should be disabled (all files are MP3)
const isFormatDisabled = files.length > 0 && files.every((f) => f.format === "mp3");
+
+ // Check if any file is FLAC (ALAC only makes sense for lossless input)
+ const hasFlacFiles = files.some((f) => f.format === "flac");
// Detect fullscreen/maximized window
useEffect(() => {
@@ -268,7 +286,7 @@ export function AudioConverterPage() {
}
};
- const addFiles = useCallback((paths: string[]) => {
+ const addFiles = useCallback(async (paths: string[]) => {
const validExtensions = [".mp3", ".flac"];
// Check for M4A files specifically
@@ -283,12 +301,19 @@ export function AudioConverterPage() {
});
}
+ // Get file sizes from backend
+ const GetFileSizes = (files: string[]): Promise> =>
+ (window as any)["go"]["main"]["App"]["GetFileSizes"](files);
+
+ const validPaths = paths.filter((path) => {
+ const ext = path.toLowerCase().slice(path.lastIndexOf("."));
+ return validExtensions.includes(ext);
+ });
+
+ const fileSizes = validPaths.length > 0 ? await GetFileSizes(validPaths) : {};
+
setFiles((prev) => {
- const newFiles: AudioFile[] = paths
- .filter((path) => {
- const ext = path.toLowerCase().slice(path.lastIndexOf("."));
- return validExtensions.includes(ext);
- })
+ const newFiles: AudioFile[] = validPaths
.filter((path) => !prev.some((f) => f.path === path))
.map((path) => {
const name = path.split(/[/\\]/).pop() || path;
@@ -297,6 +322,7 @@ export function AudioConverterPage() {
path,
name,
format: ext,
+ size: fileSizes[path] || 0,
status: "pending" as const,
};
});
@@ -598,8 +624,8 @@ export function AudioConverterPage() {
- {/* Codec selection for M4A */}
- {outputFormat === "m4a" && (
+ {/* Codec selection for M4A - only show ALAC option when input has FLAC files */}
+ {outputFormat === "m4a" && hasFlacFiles && (
)}
+
+ {formatFileSize(file.size)}
+
{file.format}
diff --git a/frontend/src/components/FileManagerPage.tsx b/frontend/src/components/FileManagerPage.tsx
index 8383bc8..892e96e 100644
--- a/frontend/src/components/FileManagerPage.tsx
+++ b/frontend/src/components/FileManagerPage.tsx
@@ -76,7 +76,11 @@ const FORMAT_PRESETS: Record = {
"track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
"title-album-artist": { label: "Title - Album Artist", template: "{title} - {album_artist}" },
"track-title-album-artist": { label: "Track. Title - Album Artist", template: "{track}. {title} - {album_artist}" },
- "custom": { label: "Custom...", template: "" },
+ "artist-album-title": { label: "Artist - Album - Title", template: "{artist} - {album} - {title}" },
+ "track-dash-title": { label: "Track - Title", template: "{track} - {title}" },
+ "disc-track-title": { label: "Disc-Track. Title", template: "{disc}-{track}. {title}" },
+ "disc-track-title-artist": { label: "Disc-Track. Title - Artist", template: "{disc}-{track}. {title} - {artist}" },
+ "custom": { label: "Custom...", template: "{title} - {artist}" },
};
const STORAGE_KEY = "spotiflac_file_manager_state";
@@ -128,7 +132,7 @@ export function FileManagerPage() {
return DEFAULT_CUSTOM_FORMAT;
});
- const renameFormat = formatPreset === "custom" ? customFormat : FORMAT_PRESETS[formatPreset].template;
+ const renameFormat = formatPreset === "custom" ? (customFormat || FORMAT_PRESETS["custom"].template) : FORMAT_PRESETS[formatPreset].template;
const [showPreview, setShowPreview] = useState(false);
const [previewData, setPreviewData] = useState([]);
const [renaming, setRenaming] = useState(false);
diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx
index 6ff4c39..9e4e2cd 100644
--- a/frontend/src/components/SettingsPage.tsx
+++ b/frontend/src/components/SettingsPage.tsx
@@ -325,7 +325,7 @@ export function SettingsPage() {
setTempSettings(prev => ({
...prev,
folderPreset: value,
- folderTemplate: value === "custom" ? prev.folderTemplate : preset.template
+ folderTemplate: value === "custom" ? (prev.folderTemplate || preset.template) : preset.template
}));
}}
>
@@ -377,7 +377,7 @@ export function SettingsPage() {
setTempSettings(prev => ({
...prev,
filenamePreset: value,
- filenameTemplate: value === "custom" ? prev.filenameTemplate : preset.template
+ filenameTemplate: value === "custom" ? (prev.filenameTemplate || preset.template) : preset.template
}));
}}
>
diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx
index 5ec5d52..0863e40 100644
--- a/frontend/src/components/ui/alert-dialog.tsx
+++ b/frontend/src/components/ui/alert-dialog.tsx
@@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
-const AlertDialog = AlertDialogPrimitive.Root
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogPortal = AlertDialogPrimitive.Portal
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
- ) {
+ return (
+
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+ )
+}
-const AlertDialogHeader = ({
+function AlertDialogContent({
className,
...props
-}: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
-const AlertDialogFooter = ({
+function AlertDialogHeader({
className,
...props
-}: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
export {
AlertDialog,
diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts
index 529cf4b..e858778 100644
--- a/frontend/src/lib/settings.ts
+++ b/frontend/src/lib/settings.ts
@@ -3,10 +3,10 @@ import { GetDefaults } from "../../wailsjs/go/main/App";
export type FontFamily = "google-sans" | "inter" | "poppins" | "roboto" | "dm-sans" | "plus-jakarta-sans" | "manrope" | "space-grotesk" | "noto-sans" | "nunito-sans" | "figtree" | "raleway" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-sans";
// Folder structure presets
-export type FolderPreset = "none" | "artist" | "album" | "artist-album" | "artist-year-album" | "album-artist" | "album-artist-album" | "album-artist-year-album" | "custom";
+export type FolderPreset = "none" | "artist" | "album" | "year-album" | "year-artist-album" | "artist-album" | "artist-year-album" | "artist-year-nested-album" | "album-artist" | "album-artist-album" | "album-artist-year-album" | "album-artist-year-nested-album" | "year" | "year-artist" | "custom";
// Filename format presets
-export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-title" | "track-title-artist" | "track-artist-title" | "title-album-artist" | "track-title-album-artist" | "custom";
+export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-title" | "track-title-artist" | "track-artist-title" | "title-album-artist" | "track-title-album-artist" | "artist-album-title" | "track-dash-title" | "disc-track-title" | "disc-track-title-artist" | "custom";
export interface Settings {
downloadPath: string;
@@ -38,12 +38,18 @@ export const FOLDER_PRESETS: Record