From a49bb560bd0e1a4955d73c1c6b189336e969b1c9 Mon Sep 17 00:00:00 2001 From: Ahmed Alghafri <50543745+ahmed-ghaf-404@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:36:25 -0700 Subject: [PATCH] Add cross-platform path handling (#89) Add cross-platform path handling support - Add sanitizePath, joinPath, buildOutputPath utilities - Add operatingSystem to Settings interface - Replace hardcoded Windows paths with dynamic path handling - Support Windows, Linux, and macOS --- frontend/src/App.tsx | 31 +++++++++++++++---------------- frontend/src/lib/settings.ts | 9 +++++++-- frontend/src/lib/utils.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2e9cfcf..730ad90 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,6 +36,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { Spinner } from "@/components/ui/spinner"; +import { joinPath, sanitizePath } from "./lib/utils"; function App() { const [spotifyUrl, setSpotifyUrl] = useState(""); @@ -125,32 +126,30 @@ function App() { const query = trackName && artistName ? `${trackName} ${artistName}` : undefined; // Build output directory based on settings + const os = settings.operatingSystem; + + // Base download path let outputDir = settings.downloadPath; - + // For playlist or artist discography downloads + + // Playlist or discography if (playlistName) { - const sanitizedPlaylist = playlistName.replace(/[<>:"/\\|?*]/g, '_').trim(); - outputDir = `${settings.downloadPath}\\${sanitizedPlaylist}`; - - // For artist discography: only use album subfolder (artist is redundant) + outputDir = joinPath(os, outputDir, sanitizePath(playlistName, os)); + if (isArtistDiscography) { - // Only add album subfolder if enabled + // Only album subfolder if (settings.albumSubfolder && albumName) { - const sanitizedAlbum = albumName.replace(/[<>:"/\\|?*]/g, '_').trim(); - outputDir = `${outputDir}\\${sanitizedAlbum}`; + outputDir = joinPath(os, outputDir, sanitizePath(albumName, os)); } } else { - // For playlist: use both artist and album subfolders if enabled - // Add artist subfolder if enabled + // Playlist rules: if (settings.artistSubfolder && artistName) { - const sanitizedArtist = artistName.replace(/[<>:"/\\|?*]/g, '_').trim(); - outputDir = `${outputDir}\\${sanitizedArtist}`; + outputDir = joinPath(os, outputDir, sanitizePath(artistName, os)); } - - // Add album subfolder if enabled + if (settings.albumSubfolder && albumName) { - const sanitizedAlbum = albumName.replace(/[<>:"/\\|?*]/g, '_').trim(); - outputDir = `${outputDir}\\${sanitizedAlbum}`; + outputDir = joinPath(os, outputDir, sanitizePath(albumName, os)); } } } diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index 678cdcd..dffb8be 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -9,6 +9,7 @@ export interface Settings { artistSubfolder: boolean; albumSubfolder: boolean; trackNumber: boolean; + operatingSystem: "Windows" | "linux/MacOS" } export const DEFAULT_SETTINGS: Settings = { @@ -20,16 +21,20 @@ export const DEFAULT_SETTINGS: Settings = { artistSubfolder: false, albumSubfolder: false, trackNumber: false, + operatingSystem: "Windows" }; +// TODO: add mac/linux defaults +const DEFAULT_PATH : string = "C:\\Users\\Public\\Music"; + async function fetchDefaultPath(): Promise { try { const data = await GetDefaults(); - return data.downloadPath || "C:\\Users\\Public\\Music"; + return data.downloadPath || DEFAULT_PATH; } catch (error) { console.error("Failed to fetch default path:", error); } - return "C:\\Users\\Public\\Music"; + return DEFAULT_PATH; } const SETTINGS_KEY = "spotiflac-settings"; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bd0c391..b8f698b 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,6 +1,35 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" +import type { Settings } from "./settings"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + + +export function sanitizePath(input: string, os: string): string { + if (os === "Windows") { + return input.replace(/[<>:"/\\|?*]/g, "_"); + } + + // unix-based OS + return input.replace(/\//g, "_"); +} + +export function joinPath(os: string, ...parts: string[]): string { + const sep = os === "Windows" ? "\\" : "/"; + + return parts + .filter(Boolean) + .map(p => p.replace(/^[/\\]+|[/\\]+$/g, "")) + .join(sep); +} + +export function buildOutputPath(settings: Settings, folder?: string) { + const os = settings.operatingSystem; + + const base = settings.downloadPath || ""; + const sanitized = folder ? sanitizePath(folder, os) : undefined; + + return sanitized ? joinPath(os, base, sanitized) : base; +} \ No newline at end of file