332 lines
13 KiB
TypeScript
332 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { SettingsSection, SettingsRow, SettingsToggle } from "../ui";
|
|
import { SystemSettings } from "../../types";
|
|
import { api } from "@/lib/api";
|
|
import { useQueryClient, useQuery } from "@tanstack/react-query";
|
|
import { CheckCircle, Loader2, User, Heart, Activity } from "lucide-react";
|
|
|
|
interface CacheSectionProps {
|
|
settings: SystemSettings;
|
|
onUpdate: (updates: Partial<SystemSettings>) => void;
|
|
}
|
|
|
|
// Progress bar component
|
|
function ProgressBar({
|
|
progress,
|
|
color = "bg-[#ecb200]",
|
|
showPercentage = true
|
|
}: {
|
|
progress: number;
|
|
color?: string;
|
|
showPercentage?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="flex items-center gap-2 flex-1">
|
|
<div className="flex-1 h-1.5 bg-white/10 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${color} transition-all duration-500 ease-out`}
|
|
style={{ width: `${Math.min(100, progress)}%` }}
|
|
/>
|
|
</div>
|
|
{showPercentage && (
|
|
<span className="text-xs text-white/50 w-10 text-right">{progress}%</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Enrichment stage component
|
|
function EnrichmentStage({
|
|
icon: Icon,
|
|
label,
|
|
description,
|
|
completed,
|
|
total,
|
|
progress,
|
|
isBackground = false,
|
|
failed = 0,
|
|
processing = 0,
|
|
}: {
|
|
icon: React.ElementType;
|
|
label: string;
|
|
description: string;
|
|
completed: number;
|
|
total: number;
|
|
progress: number;
|
|
isBackground?: boolean;
|
|
failed?: number;
|
|
processing?: number;
|
|
}) {
|
|
const isComplete = progress === 100;
|
|
const hasActivity = processing > 0;
|
|
|
|
return (
|
|
<div className="flex items-start gap-3 py-2">
|
|
<div className={`mt-0.5 p-1.5 rounded-lg ${isComplete ? 'bg-green-500/20' : 'bg-white/5'}`}>
|
|
{isComplete ? (
|
|
<CheckCircle className="w-4 h-4 text-green-400" />
|
|
) : hasActivity ? (
|
|
<Loader2 className="w-4 h-4 text-[#ecb200] animate-spin" />
|
|
) : (
|
|
<Icon className="w-4 h-4 text-white/40" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm font-medium text-white">{label}</span>
|
|
{isBackground && !isComplete && (
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-white/10 text-white/50">
|
|
background
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-white/40 mt-0.5">{description}</p>
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<ProgressBar
|
|
progress={progress}
|
|
color={isComplete ? "bg-green-500" : isBackground ? "bg-purple-500" : "bg-[#ecb200]"}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-3 mt-1 text-[10px] text-white/30">
|
|
<span>{completed} / {total}</span>
|
|
{processing > 0 && <span className="text-[#ecb200]">{processing} processing</span>}
|
|
{failed > 0 && <span className="text-red-400">{failed} failed</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function CacheSection({ settings, onUpdate }: CacheSectionProps) {
|
|
const [syncing, setSyncing] = useState(false);
|
|
const [clearingCaches, setClearingCaches] = useState(false);
|
|
const [reEnriching, setReEnriching] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const queryClient = useQueryClient();
|
|
|
|
// Fetch enrichment progress
|
|
const { data: enrichmentProgress, refetch: refetchProgress } = useQuery({
|
|
queryKey: ["enrichment-progress"],
|
|
queryFn: () => api.getEnrichmentProgress(),
|
|
refetchInterval: 5000, // Refresh every 5 seconds
|
|
staleTime: 2000,
|
|
});
|
|
|
|
const refreshNotifications = () => {
|
|
queryClient.invalidateQueries({ queryKey: ["notifications"] });
|
|
queryClient.invalidateQueries({ queryKey: ["unread-notification-count"] });
|
|
window.dispatchEvent(new CustomEvent("notifications-changed"));
|
|
};
|
|
|
|
const handleSyncAndEnrich = async () => {
|
|
setSyncing(true);
|
|
setError(null);
|
|
try {
|
|
if (settings.autoEnrichMetadata) {
|
|
await api.post("/audiobooks/sync", {});
|
|
}
|
|
await api.post("/podcasts/sync-covers", {});
|
|
await api.startLibraryEnrichment();
|
|
refreshNotifications();
|
|
refetchProgress();
|
|
} catch (err) {
|
|
console.error("Sync error:", err);
|
|
setError("Failed to sync");
|
|
} finally {
|
|
setSyncing(false);
|
|
}
|
|
};
|
|
|
|
const handleFullEnrichment = async () => {
|
|
setReEnriching(true);
|
|
setError(null);
|
|
try {
|
|
await api.triggerFullEnrichment();
|
|
refreshNotifications();
|
|
refetchProgress();
|
|
} catch (err) {
|
|
console.error("Full enrichment error:", err);
|
|
setError("Failed to start full enrichment");
|
|
} finally {
|
|
setReEnriching(false);
|
|
}
|
|
};
|
|
|
|
const handleClearCaches = async () => {
|
|
setClearingCaches(true);
|
|
setError(null);
|
|
try {
|
|
await api.clearAllCaches();
|
|
refreshNotifications();
|
|
} catch (err) {
|
|
setError("Failed to clear caches");
|
|
} finally {
|
|
setClearingCaches(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SettingsSection id="cache" title="Cache & Automation">
|
|
{/* Enrichment Progress */}
|
|
{enrichmentProgress && (
|
|
<div className="mb-6 p-4 bg-white/5 rounded-lg border border-white/10">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-sm font-medium text-white">Library Enrichment</h3>
|
|
{enrichmentProgress.coreComplete && !enrichmentProgress.isFullyComplete && (
|
|
<span className="text-xs text-purple-400 flex items-center gap-1">
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
Audio analysis running
|
|
</span>
|
|
)}
|
|
{enrichmentProgress.isFullyComplete && (
|
|
<span className="text-xs text-green-400 flex items-center gap-1">
|
|
<CheckCircle className="w-3 h-3" />
|
|
Complete
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<EnrichmentStage
|
|
icon={User}
|
|
label="Artist Metadata"
|
|
description="Bios, images, and similar artists from Last.fm"
|
|
completed={enrichmentProgress.artists.completed}
|
|
total={enrichmentProgress.artists.total}
|
|
progress={enrichmentProgress.artists.progress}
|
|
failed={enrichmentProgress.artists.failed}
|
|
/>
|
|
|
|
<EnrichmentStage
|
|
icon={Heart}
|
|
label="Mood Tags"
|
|
description="Vibes and mood data from Last.fm"
|
|
completed={enrichmentProgress.trackTags.enriched}
|
|
total={enrichmentProgress.trackTags.total}
|
|
progress={enrichmentProgress.trackTags.progress}
|
|
/>
|
|
|
|
<EnrichmentStage
|
|
icon={Activity}
|
|
label="Audio Analysis"
|
|
description="BPM, key, energy, and danceability from audio files"
|
|
completed={enrichmentProgress.audioAnalysis.completed}
|
|
total={enrichmentProgress.audioAnalysis.total}
|
|
progress={enrichmentProgress.audioAnalysis.progress}
|
|
processing={enrichmentProgress.audioAnalysis.processing}
|
|
failed={enrichmentProgress.audioAnalysis.failed}
|
|
isBackground={true}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2 mt-4 pt-3 border-t border-white/10">
|
|
<button
|
|
onClick={handleSyncAndEnrich}
|
|
disabled={syncing || reEnriching}
|
|
className="px-3 py-1.5 text-xs bg-white text-black font-medium rounded-full
|
|
hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed transition-transform"
|
|
>
|
|
{syncing ? "Syncing..." : "Sync New"}
|
|
</button>
|
|
<button
|
|
onClick={handleFullEnrichment}
|
|
disabled={syncing || reEnriching}
|
|
className="px-3 py-1.5 text-xs bg-[#333] text-white rounded-full
|
|
hover:bg-[#404040] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{reEnriching ? "Starting..." : "Re-enrich All"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cache Sizes */}
|
|
<SettingsRow
|
|
label="User cache size"
|
|
description="Maximum storage for offline content"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<input
|
|
type="range"
|
|
min={512}
|
|
max={20480}
|
|
step={512}
|
|
value={settings.maxCacheSizeMb}
|
|
onChange={(e) => onUpdate({ maxCacheSizeMb: parseInt(e.target.value) })}
|
|
className="w-32 h-1 bg-[#404040] rounded-lg appearance-none cursor-pointer
|
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
|
|
/>
|
|
<span className="text-sm text-white w-16 text-right">
|
|
{(settings.maxCacheSizeMb / 1024).toFixed(1)} GB
|
|
</span>
|
|
</div>
|
|
</SettingsRow>
|
|
|
|
<SettingsRow
|
|
label="Transcode cache size"
|
|
description="Server restart required for changes"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<input
|
|
type="range"
|
|
min={1}
|
|
max={50}
|
|
value={settings.transcodeCacheMaxGb}
|
|
onChange={(e) => onUpdate({ transcodeCacheMaxGb: parseInt(e.target.value) })}
|
|
className="w-32 h-1 bg-[#404040] rounded-lg appearance-none cursor-pointer
|
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"
|
|
/>
|
|
<span className="text-sm text-white w-16 text-right">
|
|
{settings.transcodeCacheMaxGb} GB
|
|
</span>
|
|
</div>
|
|
</SettingsRow>
|
|
|
|
{/* Automation */}
|
|
<SettingsRow
|
|
label="Auto sync library"
|
|
description="Automatically sync library changes"
|
|
htmlFor="auto-sync"
|
|
>
|
|
<SettingsToggle
|
|
id="auto-sync"
|
|
checked={settings.autoSync}
|
|
onChange={(checked) => onUpdate({ autoSync: checked })}
|
|
/>
|
|
</SettingsRow>
|
|
|
|
<SettingsRow
|
|
label="Auto enrich metadata"
|
|
description="Automatically enrich metadata for new content"
|
|
htmlFor="auto-enrich"
|
|
>
|
|
<SettingsToggle
|
|
id="auto-enrich"
|
|
checked={settings.autoEnrichMetadata}
|
|
onChange={(checked) => onUpdate({ autoEnrichMetadata: checked })}
|
|
/>
|
|
</SettingsRow>
|
|
|
|
{/* Cache Actions */}
|
|
<div className="flex flex-col gap-3 pt-4">
|
|
<button
|
|
onClick={handleClearCaches}
|
|
disabled={clearingCaches}
|
|
className="px-4 py-1.5 text-sm bg-[#333] text-white rounded-full w-fit
|
|
hover:bg-[#404040] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{clearingCaches ? "Clearing..." : "Clear All Caches"}
|
|
</button>
|
|
{error && (
|
|
<p className="text-sm text-red-400">{error}</p>
|
|
)}
|
|
</div>
|
|
</SettingsSection>
|
|
);
|
|
}
|
|
|