From 0c1c420fd8c43ffe5b84770e0f6622ae0afd9940 Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 10 Aug 2025 09:48:04 +0200 Subject: [PATCH] feat: add sound scanning and normalization features to SettingsPage; implement UI components and state management --- src/lib/api/services/admin.ts | 57 ++++++++ src/pages/admin/SettingsPage.tsx | 234 ++++++++++++++++++++++++++++++- 2 files changed, 286 insertions(+), 5 deletions(-) diff --git a/src/lib/api/services/admin.ts b/src/lib/api/services/admin.ts index 5825a06..27ad961 100644 --- a/src/lib/api/services/admin.ts +++ b/src/lib/api/services/admin.ts @@ -23,6 +23,36 @@ export interface MessageResponse { message: string } +export interface ScanResults { + added: number + updated: number + deleted: number + skipped: number + errors: string[] + files_added: string[] + files_updated: string[] + files_deleted: string[] + files_skipped: string[] +} + +export interface NormalizationResults { + processed: number + normalized: number + skipped: number + errors: number + error_details: string[] +} + +export interface ScanResponse { + message: string + results: ScanResults +} + +export interface NormalizationResponse { + message: string + results: NormalizationResults +} + export class AdminService { async listUsers(limit = 100, offset = 0): Promise { return apiClient.get(`/api/v1/admin/users/`, { @@ -49,6 +79,33 @@ export class AdminService { async listPlans(): Promise { return apiClient.get(`/api/v1/admin/users/plans/list`) } + + // Sound Management + async scanSounds(): Promise { + return apiClient.post(`/api/v1/admin/sounds/scan`) + } + + async normalizeAllSounds(force = false, onePass?: boolean): Promise { + const params = new URLSearchParams() + if (force) params.append('force', 'true') + if (onePass !== undefined) params.append('one_pass', onePass.toString()) + + const queryString = params.toString() + const url = queryString ? `/api/v1/admin/sounds/normalize/all?${queryString}` : `/api/v1/admin/sounds/normalize/all` + + return apiClient.post(url) + } + + async normalizeSoundsByType(soundType: 'SDB' | 'TTS' | 'EXT', force = false, onePass?: boolean): Promise { + const params = new URLSearchParams() + if (force) params.append('force', 'true') + if (onePass !== undefined) params.append('one_pass', onePass.toString()) + + const queryString = params.toString() + const url = queryString ? `/api/v1/admin/sounds/normalize/type/${soundType}?${queryString}` : `/api/v1/admin/sounds/normalize/type/${soundType}` + + return apiClient.post(url) + } } export const adminService = new AdminService() \ No newline at end of file diff --git a/src/pages/admin/SettingsPage.tsx b/src/pages/admin/SettingsPage.tsx index 8960f62..7ec8370 100644 --- a/src/pages/admin/SettingsPage.tsx +++ b/src/pages/admin/SettingsPage.tsx @@ -1,6 +1,77 @@ +import { useState } from 'react' import { AppLayout } from '@/components/AppLayout' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { toast } from 'sonner' +import { + Scan, + Volume2, + Settings as SettingsIcon, + Loader2, + FolderSync, + AudioWaveform +} from 'lucide-react' +import { adminService, type ScanResponse, type NormalizationResponse } from '@/lib/api/services/admin' export function SettingsPage() { + // Sound scanning state + const [scanningInProgress, setScanningInProgress] = useState(false) + const [lastScanResults, setLastScanResults] = useState(null) + + // Sound normalization state + const [normalizationInProgress, setNormalizationInProgress] = useState(false) + const [normalizationOptions, setNormalizationOptions] = useState({ + force: false, + onePass: false, + soundType: 'all' as 'all' | 'SDB' | 'TTS' | 'EXT' + }) + const [lastNormalizationResults, setLastNormalizationResults] = useState(null) + + const handleScanSounds = async () => { + setScanningInProgress(true) + try { + const response = await adminService.scanSounds() + setLastScanResults(response) + toast.success(`Sound scan completed! Added: ${response.results.added}, Updated: ${response.results.updated}, Deleted: ${response.results.deleted}`) + } catch (error) { + toast.error('Failed to scan sounds') + console.error('Sound scan error:', error) + } finally { + setScanningInProgress(false) + } + } + + const handleNormalizeSounds = async () => { + setNormalizationInProgress(true) + try { + let response: NormalizationResponse + + if (normalizationOptions.soundType === 'all') { + response = await adminService.normalizeAllSounds( + normalizationOptions.force, + normalizationOptions.onePass + ) + } else { + response = await adminService.normalizeSoundsByType( + normalizationOptions.soundType, + normalizationOptions.force, + normalizationOptions.onePass + ) + } + + setLastNormalizationResults(response) + toast.success(`Sound normalization completed! Processed: ${response.results.processed}, Normalized: ${response.results.normalized}`) + } catch (error) { + toast.error('Failed to normalize sounds') + console.error('Sound normalization error:', error) + } finally { + setNormalizationInProgress(false) + } + } + return ( -
-

System Settings

-

- System administration interface coming soon... -

+
+
+

System Settings

+ +
+ +
+ {/* Sound Scanning */} + + + + + Sound Scanning + + + +

+ Scan the sound directories to synchronize new, updated, and deleted audio files with the database. +

+ + + + {lastScanResults && ( +
+
Last Scan Results:
+
+
✅ Added: {lastScanResults.results.added}
+
🔄 Updated: {lastScanResults.results.updated}
+
🗑️ Deleted: {lastScanResults.results.deleted}
+
⏭️ Skipped: {lastScanResults.results.skipped}
+ {lastScanResults.results.errors.length > 0 && ( +
❌ Errors: {lastScanResults.results.errors.length}
+ )} +
+
+ )} +
+
+ + {/* Sound Normalization */} + + + + + Sound Normalization + + + +

+ Normalize audio levels across all sounds using FFmpeg's loudnorm filter for consistent volume. +

+ +
+
+ + +
+ +
+ + setNormalizationOptions(prev => ({ ...prev, force: !!checked })) + } + /> + +
+ +
+ + setNormalizationOptions(prev => ({ ...prev, onePass: !!checked })) + } + /> + +
+
+ + + + {lastNormalizationResults && ( +
+
Last Normalization Results:
+
+
🔄 Processed: {lastNormalizationResults.results.processed}
+
✅ Normalized: {lastNormalizationResults.results.normalized}
+
⏭️ Skipped: {lastNormalizationResults.results.skipped}
+
❌ Errors: {lastNormalizationResults.results.errors}
+ {lastNormalizationResults.results.error_details.length > 0 && ( +
+ View Error Details +
+ {lastNormalizationResults.results.error_details.map((error, index) => ( +
• {error}
+ ))} +
+
+ )} +
+
+ )} +
+
+
)