186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
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<string, unknown>, key: string) => {
|
|
const camelKey = snakeToCamel(key);
|
|
acc[camelKey] = convertKeysToCamelCase((obj as Record<string, unknown>)[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<FlatAppSettings> => {
|
|
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 <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
|
|
}
|