fix: config page
This commit is contained in:
@@ -40,6 +40,90 @@ NOTIFY_PARAMETERS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Helper functions to get final merged configs (simulate save without actually saving)
|
||||||
|
def get_final_main_config(new_config_data: dict) -> dict:
|
||||||
|
"""Returns the final main config that will be saved after merging with new_config_data."""
|
||||||
|
try:
|
||||||
|
# Load current or default config
|
||||||
|
existing_config = {}
|
||||||
|
if MAIN_CONFIG_FILE_PATH.exists():
|
||||||
|
with open(MAIN_CONFIG_FILE_PATH, "r") as f_read:
|
||||||
|
existing_config = json.load(f_read)
|
||||||
|
else:
|
||||||
|
existing_config = DEFAULT_MAIN_CONFIG.copy()
|
||||||
|
|
||||||
|
# Update with new data
|
||||||
|
for key, value in new_config_data.items():
|
||||||
|
existing_config[key] = value
|
||||||
|
|
||||||
|
# Migration: unify legacy keys to camelCase
|
||||||
|
_migrate_legacy_keys_inplace(existing_config)
|
||||||
|
|
||||||
|
# Ensure all default keys are still there
|
||||||
|
for default_key, default_value in DEFAULT_MAIN_CONFIG.items():
|
||||||
|
if default_key not in existing_config:
|
||||||
|
existing_config[default_key] = default_value
|
||||||
|
|
||||||
|
return existing_config
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating final main config: {e}", exc_info=True)
|
||||||
|
return DEFAULT_MAIN_CONFIG.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def get_final_watch_config(new_watch_config_data: dict) -> dict:
|
||||||
|
"""Returns the final watch config that will be saved after merging with new_watch_config_data."""
|
||||||
|
try:
|
||||||
|
# Load current main config
|
||||||
|
main_cfg: dict = {}
|
||||||
|
if WATCH_MAIN_CONFIG_FILE_PATH.exists():
|
||||||
|
with open(WATCH_MAIN_CONFIG_FILE_PATH, "r") as f:
|
||||||
|
main_cfg = json.load(f) or {}
|
||||||
|
else:
|
||||||
|
main_cfg = DEFAULT_MAIN_CONFIG.copy()
|
||||||
|
|
||||||
|
# Get and update watch config
|
||||||
|
watch_value = main_cfg.get("watch")
|
||||||
|
current_watch = (
|
||||||
|
watch_value.copy() if isinstance(watch_value, dict) else {}
|
||||||
|
).copy()
|
||||||
|
current_watch.update(new_watch_config_data or {})
|
||||||
|
|
||||||
|
# Ensure defaults
|
||||||
|
for k, v in DEFAULT_WATCH_CONFIG.items():
|
||||||
|
if k not in current_watch:
|
||||||
|
current_watch[k] = v
|
||||||
|
|
||||||
|
return current_watch
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating final watch config: {e}", exc_info=True)
|
||||||
|
return DEFAULT_WATCH_CONFIG.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def get_final_main_config_for_watch(new_watch_config_data: dict) -> dict:
|
||||||
|
"""Returns the final main config when updating watch config."""
|
||||||
|
try:
|
||||||
|
# Load current main config
|
||||||
|
main_cfg: dict = {}
|
||||||
|
if WATCH_MAIN_CONFIG_FILE_PATH.exists():
|
||||||
|
with open(WATCH_MAIN_CONFIG_FILE_PATH, "r") as f:
|
||||||
|
main_cfg = json.load(f) or {}
|
||||||
|
else:
|
||||||
|
main_cfg = DEFAULT_MAIN_CONFIG.copy()
|
||||||
|
|
||||||
|
# Migrate legacy keys
|
||||||
|
_migrate_legacy_keys_inplace(main_cfg)
|
||||||
|
|
||||||
|
# Ensure all default keys are still there
|
||||||
|
for default_key, default_value in DEFAULT_MAIN_CONFIG.items():
|
||||||
|
if default_key not in main_cfg:
|
||||||
|
main_cfg[default_key] = default_value
|
||||||
|
|
||||||
|
return main_cfg
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating final main config for watch: {e}", exc_info=True)
|
||||||
|
return DEFAULT_MAIN_CONFIG.copy()
|
||||||
|
|
||||||
|
|
||||||
# Helper function to check if credentials exist for a service
|
# Helper function to check if credentials exist for a service
|
||||||
def has_credentials(service: str) -> bool:
|
def has_credentials(service: str) -> bool:
|
||||||
"""Check if credentials exist for the specified service (spotify or deezer)."""
|
"""Check if credentials exist for the specified service (spotify or deezer)."""
|
||||||
@@ -68,8 +152,11 @@ def validate_config(config_data: dict, watch_config: dict = None) -> tuple[bool,
|
|||||||
Returns (is_valid, error_message).
|
Returns (is_valid, error_message).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get current watch config if not provided
|
# Get final merged watch config for validation
|
||||||
if watch_config is None:
|
if watch_config is None:
|
||||||
|
if "watch" in config_data:
|
||||||
|
watch_config = get_final_watch_config(config_data["watch"])
|
||||||
|
else:
|
||||||
watch_config = get_watch_config_http()
|
watch_config = get_watch_config_http()
|
||||||
|
|
||||||
# Ensure realTimeMultiplier is a valid integer in range 0..10 if provided
|
# Ensure realTimeMultiplier is a valid integer in range 0..10 if provided
|
||||||
@@ -137,9 +224,9 @@ def validate_watch_config(
|
|||||||
Returns (is_valid, error_message).
|
Returns (is_valid, error_message).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get current main config if not provided
|
# Get final merged main config for validation
|
||||||
if main_config is None:
|
if main_config is None:
|
||||||
main_config = get_config()
|
main_config = get_final_main_config_for_watch(watch_data)
|
||||||
|
|
||||||
# Check if trying to enable watch without download methods
|
# Check if trying to enable watch without download methods
|
||||||
if watch_data.get("enabled", False):
|
if watch_data.get("enabled", False):
|
||||||
|
|||||||
@@ -1305,11 +1305,35 @@ def _fetch_artist_discography_page(artist_id: str, limit: int, offset: int) -> d
|
|||||||
for key in ("album_group", "single_group", "compilation_group", "appears_on_group"):
|
for key in ("album_group", "single_group", "compilation_group", "appears_on_group"):
|
||||||
grp = artist.get(key)
|
grp = artist.get(key)
|
||||||
if isinstance(grp, list):
|
if isinstance(grp, list):
|
||||||
all_items.extend(grp)
|
# Check if items are strings (IDs) or dictionaries (metadata)
|
||||||
|
if grp and isinstance(grp[0], str):
|
||||||
|
# Items are album IDs as strings, fetch metadata for each
|
||||||
|
for album_id in grp:
|
||||||
|
try:
|
||||||
|
album_data = client.get_album(album_id, include_tracks=False)
|
||||||
|
if album_data:
|
||||||
|
# Add the album_group type for filtering
|
||||||
|
album_data["album_group"] = key.replace("_group", "")
|
||||||
|
all_items.append(album_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to fetch album {album_id}: {e}")
|
||||||
|
else:
|
||||||
|
# Items are already dictionaries (album metadata)
|
||||||
|
for item in grp:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# Ensure album_group is set for filtering
|
||||||
|
if "album_group" not in item:
|
||||||
|
item["album_group"] = key.replace("_group", "")
|
||||||
|
all_items.append(item)
|
||||||
elif isinstance(grp, dict):
|
elif isinstance(grp, dict):
|
||||||
items = grp.get("items") or grp.get("releases") or []
|
items = grp.get("items") or grp.get("releases") or []
|
||||||
if isinstance(items, list):
|
if isinstance(items, list):
|
||||||
all_items.extend(items)
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# Ensure album_group is set for filtering
|
||||||
|
if "album_group" not in item:
|
||||||
|
item["album_group"] = key.replace("_group", "")
|
||||||
|
all_items.append(item)
|
||||||
total = len(all_items)
|
total = len(all_items)
|
||||||
start = max(0, offset or 0)
|
start = max(0, offset or 0)
|
||||||
end = start + max(1, limit or 50)
|
end = start + max(1, limit or 50)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { authApiClient } from "../../lib/api-client";
|
import { authApiClient } from "../../lib/api-client";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -16,12 +16,32 @@ interface WebhookSettings {
|
|||||||
available_events: string[]; // Provided by API, not saved
|
available_events: string[]; // Provided by API, not saved
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- API Functions ---
|
interface ServerConfig {
|
||||||
const fetchSpotifyApiConfig = async (): Promise<SpotifyApiSettings> => {
|
client_id?: string;
|
||||||
const { data } = await authApiClient.client.get("/credentials/spotify_api_config");
|
client_secret?: string;
|
||||||
return data;
|
utilityConcurrency?: number;
|
||||||
|
librespotConcurrency?: number;
|
||||||
|
url?: string;
|
||||||
|
events?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchServerConfig = async (): Promise<ServerConfig> => {
|
||||||
|
const [spotifyConfig, generalConfig] = await Promise.all([
|
||||||
|
authApiClient.client.get("/credentials/spotify_api_config").catch(() => ({ data: {} })),
|
||||||
|
authApiClient.getConfig<any>(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...spotifyConfig.data,
|
||||||
|
...generalConfig,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveServerConfig = async (data: Partial<ServerConfig>) => {
|
||||||
|
const payload = { ...data };
|
||||||
|
const { data: response } = await authApiClient.client.post("/config", payload);
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
const saveSpotifyApiConfig = (data: SpotifyApiSettings) => authApiClient.client.put("/credentials/spotify_api_config", data);
|
|
||||||
|
|
||||||
const fetchWebhookConfig = async (): Promise<WebhookSettings> => {
|
const fetchWebhookConfig = async (): Promise<WebhookSettings> => {
|
||||||
// Mock a response since backend endpoint doesn't exist
|
// Mock a response since backend endpoint doesn't exist
|
||||||
@@ -32,40 +52,34 @@ const fetchWebhookConfig = async (): Promise<WebhookSettings> => {
|
|||||||
available_events: ["download_start", "download_complete", "download_failed", "watch_added"],
|
available_events: ["download_start", "download_complete", "download_failed", "watch_added"],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const saveWebhookConfig = (data: Partial<WebhookSettings>) => {
|
|
||||||
toast.info("Webhook configuration is not available.");
|
const saveWebhookConfig = async (data: Partial<WebhookSettings>) => {
|
||||||
return Promise.resolve(data);
|
const payload = { ...data };
|
||||||
|
const { data: response } = await authApiClient.client.post("/config", payload);
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testWebhook = (url: string) => {
|
const testWebhook = (url: string) => {
|
||||||
toast.info("Webhook testing is not available.");
|
toast.info("Webhook testing is not available.");
|
||||||
return Promise.resolve(url);
|
return Promise.resolve(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Components ---
|
// --- Components ---
|
||||||
function SpotifyApiForm() {
|
function SpotifyApiForm({ config, onConfigChange }: { config: ServerConfig; onConfigChange: (updates: Partial<ServerConfig>) => void }) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data, isLoading } = useQuery({ queryKey: ["spotifyApiConfig"], queryFn: fetchSpotifyApiConfig });
|
|
||||||
const { register, handleSubmit, reset } = useForm<SpotifyApiSettings>();
|
const { register, handleSubmit, reset } = useForm<SpotifyApiSettings>();
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: saveSpotifyApiConfig,
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Spotify API settings saved!");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["spotifyApiConfig"] });
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
console.error("Failed to save Spotify API settings:", (e as any).message);
|
|
||||||
toast.error(`Failed to save: ${(e as any).message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) reset(data);
|
if (config) {
|
||||||
}, [data, reset]);
|
reset({
|
||||||
|
client_id: config.client_id || "",
|
||||||
|
client_secret: config.client_secret || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [config, reset]);
|
||||||
|
|
||||||
const onSubmit = (formData: SpotifyApiSettings) => mutation.mutate(formData);
|
const onSubmit = (formData: SpotifyApiSettings) => {
|
||||||
|
onConfigChange(formData);
|
||||||
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading Spotify API settings...</p>;
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
@@ -73,15 +87,10 @@ function SpotifyApiForm() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={mutation.isPending}
|
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md"
|
||||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
title="Save Spotify API Settings"
|
||||||
title="Save Spotify API"
|
|
||||||
>
|
>
|
||||||
{mutation.isPending ? (
|
|
||||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
|
||||||
) : (
|
|
||||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,54 +119,31 @@ function SpotifyApiForm() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UtilityConcurrencyForm() {
|
function UtilityConcurrencyForm({ config, onConfigChange }: { config: ServerConfig; onConfigChange: (updates: Partial<ServerConfig>) => void }) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: configData, isLoading } = useQuery({
|
|
||||||
queryKey: ["config"],
|
|
||||||
queryFn: () => authApiClient.getConfig<any>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ utilityConcurrency: number }>();
|
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ utilityConcurrency: number }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (configData) {
|
if (config) {
|
||||||
reset({ utilityConcurrency: Number(configData.utilityConcurrency ?? 1) });
|
reset({ utilityConcurrency: Number(config.utilityConcurrency ?? 1) });
|
||||||
}
|
}
|
||||||
}, [configData, reset]);
|
}, [config, reset]);
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: (payload: { utilityConcurrency: number }) => authApiClient.updateConfig(payload),
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Utility worker concurrency saved!");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
toast.error(`Failed to save: ${(e as any).message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (values: { utilityConcurrency: number }) => {
|
const onSubmit = (values: { utilityConcurrency: number }) => {
|
||||||
const value = Math.max(1, Number(values.utilityConcurrency || 1));
|
const value = Math.max(1, Number(values.utilityConcurrency || 1));
|
||||||
mutation.mutate({ utilityConcurrency: value });
|
onConfigChange({ utilityConcurrency: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading server settings...</p>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<div className="flex items-center justify-end mb-2">
|
<div className="flex items-center justify-end mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={mutation.isPending || !isDirty}
|
disabled={!isDirty}
|
||||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||||
title="Save Utility Concurrency"
|
title="Save Utility Concurrency"
|
||||||
>
|
>
|
||||||
{mutation.isPending ? (
|
|
||||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
|
||||||
) : (
|
|
||||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,55 +165,32 @@ function UtilityConcurrencyForm() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LibrespotConcurrencyForm() {
|
function LibrespotConcurrencyForm({ config, onConfigChange }: { config: ServerConfig; onConfigChange: (updates: Partial<ServerConfig>) => void }) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: configData, isLoading } = useQuery({
|
|
||||||
queryKey: ["config"],
|
|
||||||
queryFn: () => authApiClient.getConfig<any>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ librespotConcurrency: number }>();
|
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<{ librespotConcurrency: number }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (configData) {
|
if (config) {
|
||||||
reset({ librespotConcurrency: Number(configData.librespotConcurrency ?? 2) });
|
reset({ librespotConcurrency: Number(config.librespotConcurrency ?? 2) });
|
||||||
}
|
}
|
||||||
}, [configData, reset]);
|
}, [config, reset]);
|
||||||
|
|
||||||
const mutation = useMutation({
|
|
||||||
mutationFn: (payload: { librespotConcurrency: number }) => authApiClient.updateConfig(payload),
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Librespot concurrency saved!");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
toast.error(`Failed to save: ${(e as any).message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (values: { librespotConcurrency: number }) => {
|
const onSubmit = (values: { librespotConcurrency: number }) => {
|
||||||
const raw = Number(values.librespotConcurrency || 2);
|
const raw = Number(values.librespotConcurrency || 2);
|
||||||
const safe = Math.max(1, Math.min(16, raw));
|
const safe = Math.max(1, Math.min(16, raw));
|
||||||
mutation.mutate({ librespotConcurrency: safe });
|
onConfigChange({ librespotConcurrency: safe });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) return <p className="text-content-muted dark:text-content-muted-dark">Loading server settings...</p>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<div className="flex items-center justify-end mb-2">
|
<div className="flex items-center justify-end mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={mutation.isPending || !isDirty}
|
disabled={!isDirty}
|
||||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||||
title="Save Librespot Concurrency"
|
title="Save Librespot Concurrency"
|
||||||
>
|
>
|
||||||
{mutation.isPending ? (
|
|
||||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
|
||||||
) : (
|
|
||||||
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
<img src="/save.svg" alt="Save" className="w-5 h-5 logo" />
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +256,7 @@ function WebhookForm() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
className="px-4 py-2 bg-button-primary hover:bg-button-primary-hover text-button-primary-text rounded-md disabled:opacity-50"
|
||||||
title="Save Webhook"
|
title="Save Webhook Settings"
|
||||||
>
|
>
|
||||||
{mutation.isPending ? (
|
{mutation.isPending ? (
|
||||||
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
<img src="/spinner.svg" alt="Saving" className="w-5 h-5 animate-spin logo" />
|
||||||
@@ -356,24 +319,61 @@ function WebhookForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServerTab() {
|
export function ServerTab() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [localConfig, setLocalConfig] = useState<ServerConfig>({});
|
||||||
|
|
||||||
|
const { data: serverConfig, isLoading } = useQuery({
|
||||||
|
queryKey: ["serverConfig"],
|
||||||
|
queryFn: fetchServerConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: saveServerConfig,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Server settings saved successfully!");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["serverConfig"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to save server settings", (error as any).message);
|
||||||
|
toast.error(`Failed to save server settings: ${(error as any).message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverConfig) {
|
||||||
|
setLocalConfig(serverConfig);
|
||||||
|
}
|
||||||
|
}, [serverConfig]);
|
||||||
|
|
||||||
|
const handleConfigChange = (updates: Partial<ServerConfig>) => {
|
||||||
|
const newConfig = { ...localConfig, ...updates };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
mutation.mutate(newConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading server settings...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify API</h3>
|
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Spotify API</h3>
|
||||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Provide your own API credentials to avoid rate-limiting issues.</p>
|
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Provide your own API credentials to avoid rate-limiting issues.</p>
|
||||||
<SpotifyApiForm />
|
<SpotifyApiForm config={localConfig} onConfigChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-border dark:border-border-dark" />
|
<hr className="border-border dark:border-border-dark" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Utility Worker</h3>
|
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Utility Worker</h3>
|
||||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Tune background utility worker concurrency for low-powered systems.</p>
|
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Tune background utility worker concurrency for low-powered systems.</p>
|
||||||
<UtilityConcurrencyForm />
|
<UtilityConcurrencyForm config={localConfig} onConfigChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-border dark:border-border-dark" />
|
<hr className="border-border dark:border-border-dark" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Librespot</h3>
|
<h3 className="text-xl font-semibold text-content-primary dark:text-content-primary-dark">Librespot</h3>
|
||||||
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Adjust Librespot client worker threads.</p>
|
<p className="text-sm text-content-muted dark:text-content-muted-dark mt-1">Adjust Librespot client worker threads.</p>
|
||||||
<LibrespotConcurrencyForm />
|
<LibrespotConcurrencyForm config={localConfig} onConfigChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-border dark:border-border-dark" />
|
<hr className="border-border dark:border-border-dark" />
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user