v6.9
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
<span className="text-muted-foreground">Nyquist:</span>
|
||||
<span className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</span>
|
||||
</div>
|
||||
{result.file_size > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Size:</span>
|
||||
<span className="font-semibold">{formatFileSize(result.file_size)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dynamic Range - Single line */}
|
||||
|
||||
@@ -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,11 +150,20 @@ 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(() => {
|
||||
const checkFullscreen = () => {
|
||||
@@ -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<Record<string, number>> =>
|
||||
(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() {
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
{/* Codec selection for M4A */}
|
||||
{outputFormat === "m4a" && (
|
||||
{/* Codec selection for M4A - only show ALAC option when input has FLAC files */}
|
||||
{outputFormat === "m4a" && hasFlacFiles && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="whitespace-nowrap">Codec:</Label>
|
||||
<ToggleGroup
|
||||
@@ -672,6 +698,9 @@ export function AudioConverterPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatFileSize(file.size)}
|
||||
</span>
|
||||
<span className="text-xs uppercase text-muted-foreground">
|
||||
{file.format}
|
||||
</span>
|
||||
|
||||
@@ -76,7 +76,11 @@ const FORMAT_PRESETS: Record<string, { label: string; template: string }> = {
|
||||
"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<backend.RenamePreview[]>([]);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
|
||||
@@ -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<FolderPreset, { label: string; template: str
|
||||
"none": { label: "No Subfolder", template: "" },
|
||||
"artist": { label: "Artist", template: "{artist}" },
|
||||
"album": { label: "Album", template: "{album}" },
|
||||
"year-album": { label: "[Year] Album", template: "[{year}] {album}" },
|
||||
"year-artist-album": { label: "[Year] Artist - Album", template: "[{year}] {artist} - {album}" },
|
||||
"artist-album": { label: "Artist / Album", template: "{artist}/{album}" },
|
||||
"artist-year-album": { label: "Artist / [Year] Album", template: "{artist}/[{year}] {album}" },
|
||||
"artist-year-nested-album": { label: "Artist / Year / Album", template: "{artist}/{year}/{album}" },
|
||||
"album-artist": { label: "Album Artist", template: "{album_artist}" },
|
||||
"album-artist-album": { label: "Album Artist / Album", template: "{album_artist}/{album}" },
|
||||
"album-artist-year-album": { label: "Album Artist / [Year] Album", template: "{album_artist}/[{year}] {album}" },
|
||||
"custom": { label: "Custom...", template: "" },
|
||||
"album-artist-year-nested-album": { label: "Album Artist / Year / Album", template: "{album_artist}/{year}/{album}" },
|
||||
"year": { label: "Year", template: "{year}" },
|
||||
"year-artist": { label: "Year / Artist", template: "{year}/{artist}" },
|
||||
"custom": { label: "Custom...", template: "{artist}/{album}" },
|
||||
};
|
||||
|
||||
// Filename preset templates
|
||||
@@ -56,7 +62,11 @@ export const FILENAME_PRESETS: Record<FilenamePreset, { label: string; template:
|
||||
"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}" },
|
||||
};
|
||||
|
||||
// Available template variables
|
||||
|
||||
@@ -168,6 +168,7 @@ export interface SpectrumData {
|
||||
|
||||
export interface AnalysisResult {
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
sample_rate: number;
|
||||
channels: number;
|
||||
bits_per_sample: number;
|
||||
|
||||
Reference in New Issue
Block a user