import { type ReactNode } from "react"; import { authApiClient } from "../lib/api-client"; import { SettingsContext, type AppSettings } from "./settings-context"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "./auth-context"; // --- Case Conversion Utility --- // This is added here to simplify the fix and avoid module resolution issues. function snakeToCamel(str: string): string { return str.replace(/(_\w)/g, (m) => m[1].toUpperCase()); } function convertKeysToCamelCase(obj: unknown): unknown { if (Array.isArray(obj)) { return obj.map((v) => convertKeysToCamelCase(v)); } if (typeof obj === "object" && obj !== null) { return Object.keys(obj).reduce((acc: Record, key: string) => { const camelKey = snakeToCamel(key); acc[camelKey] = convertKeysToCamelCase((obj as Record)[key]); return acc; }, {}); } return obj; } // Redefine AppSettings to match the flat structure of the API response export type FlatAppSettings = { service: "spotify" | "deezer"; spotify: string; spotifyQuality: "NORMAL" | "HIGH" | "VERY_HIGH"; deezer: string; deezerQuality: "MP3_128" | "MP3_320" | "FLAC"; maxConcurrentDownloads: number; utilityConcurrency: number; realTime: boolean; fallback: boolean; convertTo: "MP3" | "AAC" | "OGG" | "OPUS" | "FLAC" | "WAV" | "ALAC" | ""; bitrate: string; maxRetries: number; retryDelaySeconds: number; retryDelayIncrease: number; customDirFormat: string; customTrackFormat: string; tracknumPadding: boolean; saveCover: boolean; explicitFilter: boolean; // Add other fields from the old AppSettings as needed by other parts of the app watch: AppSettings["watch"]; // Add defaults for the new download properties threads: number; path: string; skipExisting: boolean; m3u: boolean; hlsThreads: number; // Frontend-only flag used in DownloadsTab recursiveQuality: boolean; separateTracksByUser: boolean; // Add defaults for the new formatting properties track: string; album: string; playlist: string; compilation: string; artistSeparator: string; spotifyMetadata: boolean; realTimeMultiplier: number; }; const defaultSettings: FlatAppSettings = { service: "spotify", spotify: "", spotifyQuality: "NORMAL", deezer: "", deezerQuality: "MP3_128", maxConcurrentDownloads: 3, utilityConcurrency: 1, realTime: false, fallback: false, convertTo: "", bitrate: "", maxRetries: 3, retryDelaySeconds: 5, retryDelayIncrease: 5, customDirFormat: "%ar_album%/%album%", customTrackFormat: "%tracknum%. %music%", tracknumPadding: true, saveCover: true, explicitFilter: false, // Add defaults for the new download properties threads: 4, path: "/downloads", skipExisting: true, m3u: false, hlsThreads: 8, // Frontend-only default recursiveQuality: false, separateTracksByUser: false, // Add defaults for the new formatting properties track: "{artist_name}/{album_name}/{track_number} - {track_name}", album: "{artist_name}/{album_name}", playlist: "Playlists/{playlist_name}", compilation: "Compilations/{album_name}", artistSeparator: "; ", spotifyMetadata: true, watch: { enabled: false, maxItemsPerRun: 50, watchPollIntervalSeconds: 3600, watchedArtistAlbumGroup: ["album", "single"], }, realTimeMultiplier: 0, }; interface FetchedCamelCaseSettings { watchEnabled?: boolean; watch?: { enabled: boolean; maxItemsPerRun?: number; watchPollIntervalSeconds?: number; watchedArtistAlbumGroup?: string[] }; [key: string]: unknown; } const fetchSettings = async (): Promise => { try { const [{ data: generalConfig }, { data: watchConfig }] = await Promise.all([ authApiClient.client.get("/config"), authApiClient.client.get("/config/watch"), ]); const combinedConfig = { ...generalConfig, watch: watchConfig, }; // Transform the keys before returning the data const camelData = convertKeysToCamelCase(combinedConfig) as FetchedCamelCaseSettings; const withDefaults: FlatAppSettings = { ...(camelData as unknown as FlatAppSettings), // Ensure required frontend-only fields exist recursiveQuality: Boolean((camelData as any).recursiveQuality ?? false), realTimeMultiplier: Number((camelData as any).realTimeMultiplier ?? 0), utilityConcurrency: Number((camelData as any).utilityConcurrency ?? 1), // Ensure watch subkeys default if missing watch: { ...(camelData.watch as any), enabled: Boolean((camelData.watch as any)?.enabled ?? false), maxItemsPerRun: Number((camelData.watch as any)?.maxItemsPerRun ?? 50), watchPollIntervalSeconds: Number((camelData.watch as any)?.watchPollIntervalSeconds ?? 3600), watchedArtistAlbumGroup: (camelData.watch as any)?.watchedArtistAlbumGroup ?? ["album", "single"], }, }; return withDefaults; } catch (error: any) { // If we get authentication errors, return default settings if (error.response?.status === 401 || error.response?.status === 403) { console.log("Authentication required for config access, using default settings"); return defaultSettings; } // Re-throw other errors for React Query to handle throw error; } }; export function SettingsProvider({ children }: { children: ReactNode }) { const { isLoading, authEnabled, isAuthenticated, user } = useAuth(); // Only fetch settings when auth is ready and user is admin (or auth is disabled) const shouldFetchSettings = !isLoading && (!authEnabled || (isAuthenticated && user?.role === "admin")); const { data: settings, isLoading: isSettingsLoading, isError, } = useQuery({ queryKey: ["config"], queryFn: fetchSettings, staleTime: 1000 * 60 * 5, // 5 minutes refetchOnWindowFocus: false, enabled: shouldFetchSettings, // Only run query when auth is ready and user is admin }); // Use default settings on error to prevent app crash const value = { settings: isError ? defaultSettings : settings || null, isLoading: isSettingsLoading }; return {children}; }