This commit is contained in:
afkarxyz
2025-12-19 16:50:43 +07:00
parent 7d0fde3acc
commit 436feb7f7c
10 changed files with 220 additions and 119 deletions
+5
View File
@@ -684,6 +684,11 @@ func (a *App) SelectAudioFiles() ([]string, error) {
return files, nil 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 // ListDirectoryFiles lists files and folders in a directory
func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) { func (a *App) ListDirectoryFiles(dirPath string) ([]backend.FileInfo, error) {
if dirPath == "" { if dirPath == "" {
+8
View File
@@ -12,6 +12,7 @@ import (
// AnalysisResult contains the audio analysis data // AnalysisResult contains the audio analysis data
type AnalysisResult struct { type AnalysisResult struct {
FilePath string `json:"file_path"` FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
SampleRate uint32 `json:"sample_rate"` SampleRate uint32 `json:"sample_rate"`
Channels uint8 `json:"channels"` Channels uint8 `json:"channels"`
BitsPerSample uint8 `json:"bits_per_sample"` 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) 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 // Parse FLAC file
f, err := flac.ParseFile(filepath) f, err := flac.ParseFile(filepath)
if err != nil { if err != nil {
@@ -38,6 +45,7 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
result := &AnalysisResult{ result := &AnalysisResult{
FilePath: filepath, FilePath: filepath,
FileSize: fileInfo.Size(),
} }
// Extract basic audio properties from STREAMINFO block // Extract basic audio properties from STREAMINFO block
+12
View File
@@ -344,6 +344,18 @@ func PreviewRename(files []string, format string) []RenamePreview {
return previews 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 // RenameFiles renames files based on their metadata
func RenameFiles(files []string, format string) []RenameResult { func RenameFiles(files []string, format string) []RenameResult {
var results []RenameResult var results []RenameResult
+17 -1
View File
@@ -8,7 +8,8 @@ import {
TrendingUp, TrendingUp,
FileAudio, FileAudio,
Clock, Clock,
Gauge Gauge,
HardDrive
} from "lucide-react"; } from "lucide-react";
import type { AnalysisResult } from "@/types/api"; import type { AnalysisResult } from "@/types/api";
@@ -78,6 +79,14 @@ export function AudioAnalysis({
return num.toFixed(2); 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) // Calculate Nyquist frequency (half of sample rate)
const nyquistFreq = result.sample_rate / 2; const nyquistFreq = result.sample_rate / 2;
@@ -117,6 +126,13 @@ export function AudioAnalysis({
<span className="text-muted-foreground">Nyquist:</span> <span className="text-muted-foreground">Nyquist:</span>
<span className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</span> <span className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</span>
</div> </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> </div>
{/* Dynamic Range - Single line */} {/* Dynamic Range - Single line */}
+38 -9
View File
@@ -30,11 +30,20 @@ interface AudioFile {
path: string; path: string;
name: string; name: string;
format: string; format: string;
size: number;
status: "pending" | "converting" | "success" | "error"; status: "pending" | "converting" | "success" | "error";
error?: string; error?: string;
outputPath?: 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 = [ const BITRATE_OPTIONS = [
{ value: "320k", label: "320k" }, { value: "320k", label: "320k" },
{ value: "256k", label: "256k" }, { value: "256k", label: "256k" },
@@ -141,11 +150,20 @@ export function AudioConverterPage() {
if (allMP3 && outputFormat !== "m4a") { if (allMP3 && outputFormat !== "m4a") {
setOutputFormat("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) // Check if format selection should be disabled (all files are MP3)
const isFormatDisabled = files.length > 0 && files.every((f) => f.format === "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 // Detect fullscreen/maximized window
useEffect(() => { useEffect(() => {
const checkFullscreen = () => { const checkFullscreen = () => {
@@ -268,7 +286,7 @@ export function AudioConverterPage() {
} }
}; };
const addFiles = useCallback((paths: string[]) => { const addFiles = useCallback(async (paths: string[]) => {
const validExtensions = [".mp3", ".flac"]; const validExtensions = [".mp3", ".flac"];
// Check for M4A files specifically // 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) => { setFiles((prev) => {
const newFiles: AudioFile[] = paths const newFiles: AudioFile[] = validPaths
.filter((path) => {
const ext = path.toLowerCase().slice(path.lastIndexOf("."));
return validExtensions.includes(ext);
})
.filter((path) => !prev.some((f) => f.path === path)) .filter((path) => !prev.some((f) => f.path === path))
.map((path) => { .map((path) => {
const name = path.split(/[/\\]/).pop() || path; const name = path.split(/[/\\]/).pop() || path;
@@ -297,6 +322,7 @@ export function AudioConverterPage() {
path, path,
name, name,
format: ext, format: ext,
size: fileSizes[path] || 0,
status: "pending" as const, status: "pending" as const,
}; };
}); });
@@ -598,8 +624,8 @@ export function AudioConverterPage() {
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
</div> </div>
{/* Codec selection for M4A */} {/* Codec selection for M4A - only show ALAC option when input has FLAC files */}
{outputFormat === "m4a" && ( {outputFormat === "m4a" && hasFlacFiles && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="whitespace-nowrap">Codec:</Label> <Label className="whitespace-nowrap">Codec:</Label>
<ToggleGroup <ToggleGroup
@@ -672,6 +698,9 @@ export function AudioConverterPage() {
</p> </p>
)} )}
</div> </div>
<span className="text-xs text-muted-foreground">
{formatFileSize(file.size)}
</span>
<span className="text-xs uppercase text-muted-foreground"> <span className="text-xs uppercase text-muted-foreground">
{file.format} {file.format}
</span> </span>
+6 -2
View File
@@ -76,7 +76,11 @@ const FORMAT_PRESETS: Record<string, { label: string; template: string }> = {
"track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" }, "track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
"title-album-artist": { label: "Title - Album Artist", template: "{title} - {album_artist}" }, "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}" }, "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"; const STORAGE_KEY = "spotiflac_file_manager_state";
@@ -128,7 +132,7 @@ export function FileManagerPage() {
return DEFAULT_CUSTOM_FORMAT; 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 [showPreview, setShowPreview] = useState(false);
const [previewData, setPreviewData] = useState<backend.RenamePreview[]>([]); const [previewData, setPreviewData] = useState<backend.RenamePreview[]>([]);
const [renaming, setRenaming] = useState(false); const [renaming, setRenaming] = useState(false);
+2 -2
View File
@@ -325,7 +325,7 @@ export function SettingsPage() {
setTempSettings(prev => ({ setTempSettings(prev => ({
...prev, ...prev,
folderPreset: value, 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 => ({ setTempSettings(prev => ({
...prev, ...prev,
filenamePreset: value, filenamePreset: value,
filenameTemplate: value === "custom" ? prev.filenameTemplate : preset.template filenameTemplate: value === "custom" ? (prev.filenameTemplate || preset.template) : preset.template
})); }));
}} }}
> >
+117 -101
View File
@@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button" 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< function AlertDialogOverlay({
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
<AlertDialogPrimitive.Overlay return (
className={cn( <AlertDialogPrimitive.Overlay
"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", data-slot="alert-dialog-overlay"
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}
className={cn( 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 className
)} )}
{...props} {...props}
/> />
</AlertDialogPortal> )
)) }
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ function AlertDialogContent({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
<div return (
className={cn( <AlertDialogPortal>
"flex flex-col space-y-2 text-center sm:text-left", <AlertDialogOverlay />
className <AlertDialogPrimitive.Content
)} data-slot="alert-dialog-content"
{...props} 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
AlertDialogHeader.displayName = "AlertDialogHeader" )}
{...props}
/>
</AlertDialogPortal>
)
}
const AlertDialogFooter = ({ function AlertDialogHeader({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.ComponentProps<"div">) {
<div return (
className={cn( <div
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", data-slot="alert-dialog-header"
className className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
)} {...props}
{...props} />
/> )
) }
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< function AlertDialogFooter({
React.ElementRef<typeof AlertDialogPrimitive.Title>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<"div">) {
<AlertDialogPrimitive.Title return (
ref={ref} <div
className={cn("text-lg font-semibold", className)} data-slot="alert-dialog-footer"
{...props} className={cn(
/> "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
)) className
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName )}
{...props}
/>
)
}
const AlertDialogDescription = React.forwardRef< function AlertDialogTitle({
React.ElementRef<typeof AlertDialogPrimitive.Description>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
<AlertDialogPrimitive.Description return (
ref={ref} <AlertDialogPrimitive.Title
className={cn("text-sm text-muted-foreground", className)} data-slot="alert-dialog-title"
{...props} className={cn("text-lg font-semibold", className)}
/> {...props}
)) />
AlertDialogDescription.displayName = )
AlertDialogPrimitive.Description.displayName }
const AlertDialogAction = React.forwardRef< function AlertDialogDescription({
React.ElementRef<typeof AlertDialogPrimitive.Action>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
<AlertDialogPrimitive.Action return (
ref={ref} <AlertDialogPrimitive.Description
className={cn(buttonVariants(), className)} data-slot="alert-dialog-description"
{...props} className={cn("text-muted-foreground text-sm", className)}
/> {...props}
)) />
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName )
}
const AlertDialogCancel = React.forwardRef< function AlertDialogAction({
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, className,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> ...props
>(({ className, ...props }, ref) => ( }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
<AlertDialogPrimitive.Cancel return (
ref={ref} <AlertDialogPrimitive.Action
className={cn( className={cn(buttonVariants(), className)}
buttonVariants({ variant: "outline" }), {...props}
"mt-2 sm:mt-0", />
className )
)} }
{...props}
/> function AlertDialogCancel({
)) className,
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export { export {
AlertDialog, AlertDialog,
+14 -4
View File
@@ -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"; 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 // 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 // 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 { export interface Settings {
downloadPath: string; downloadPath: string;
@@ -38,12 +38,18 @@ export const FOLDER_PRESETS: Record<FolderPreset, { label: string; template: str
"none": { label: "No Subfolder", template: "" }, "none": { label: "No Subfolder", template: "" },
"artist": { label: "Artist", template: "{artist}" }, "artist": { label: "Artist", template: "{artist}" },
"album": { label: "Album", template: "{album}" }, "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-album": { label: "Artist / Album", template: "{artist}/{album}" },
"artist-year-album": { label: "Artist / [Year] Album", template: "{artist}/[{year}] {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": { label: "Album Artist", template: "{album_artist}" },
"album-artist-album": { label: "Album Artist / Album", template: "{album_artist}/{album}" }, "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}" }, "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 // 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}" }, "track-artist-title": { label: "Track. Artist - Title", template: "{track}. {artist} - {title}" },
"title-album-artist": { label: "Title - Album Artist", template: "{title} - {album_artist}" }, "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}" }, "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 // Available template variables
+1
View File
@@ -168,6 +168,7 @@ export interface SpectrumData {
export interface AnalysisResult { export interface AnalysisResult {
file_path: string; file_path: string;
file_size: number;
sample_rate: number; sample_rate: number;
channels: number; channels: number;
bits_per_sample: number; bits_per_sample: number;