feat: add sound scanning and normalization features to SettingsPage; implement UI components and state management
This commit is contained in:
@@ -23,6 +23,36 @@ export interface MessageResponse {
|
|||||||
message: string
|
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 {
|
export class AdminService {
|
||||||
async listUsers(limit = 100, offset = 0): Promise<User[]> {
|
async listUsers(limit = 100, offset = 0): Promise<User[]> {
|
||||||
return apiClient.get<User[]>(`/api/v1/admin/users/`, {
|
return apiClient.get<User[]>(`/api/v1/admin/users/`, {
|
||||||
@@ -49,6 +79,33 @@ export class AdminService {
|
|||||||
async listPlans(): Promise<Plan[]> {
|
async listPlans(): Promise<Plan[]> {
|
||||||
return apiClient.get<Plan[]>(`/api/v1/admin/users/plans/list`)
|
return apiClient.get<Plan[]>(`/api/v1/admin/users/plans/list`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sound Management
|
||||||
|
async scanSounds(): Promise<ScanResponse> {
|
||||||
|
return apiClient.post<ScanResponse>(`/api/v1/admin/sounds/scan`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async normalizeAllSounds(force = false, onePass?: boolean): Promise<NormalizationResponse> {
|
||||||
|
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<NormalizationResponse>(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async normalizeSoundsByType(soundType: 'SDB' | 'TTS' | 'EXT', force = false, onePass?: boolean): Promise<NormalizationResponse> {
|
||||||
|
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<NormalizationResponse>(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adminService = new AdminService()
|
export const adminService = new AdminService()
|
||||||
@@ -1,6 +1,77 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { AppLayout } from '@/components/AppLayout'
|
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() {
|
export function SettingsPage() {
|
||||||
|
// Sound scanning state
|
||||||
|
const [scanningInProgress, setScanningInProgress] = useState(false)
|
||||||
|
const [lastScanResults, setLastScanResults] = useState<ScanResponse | null>(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<NormalizationResponse | null>(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 (
|
return (
|
||||||
<AppLayout
|
<AppLayout
|
||||||
breadcrumb={{
|
breadcrumb={{
|
||||||
@@ -11,11 +82,164 @@ export function SettingsPage() {
|
|||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
<div className="flex-1 space-y-6">
|
||||||
<h1 className="text-2xl font-bold mb-4">System Settings</h1>
|
<div className="flex justify-between items-center">
|
||||||
<p className="text-muted-foreground">
|
<h1 className="text-3xl font-bold">System Settings</h1>
|
||||||
System administration interface coming soon...
|
<SettingsIcon className="h-8 w-8" />
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
{/* Sound Scanning */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FolderSync className="h-5 w-5" />
|
||||||
|
Sound Scanning
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Scan the sound directories to synchronize new, updated, and deleted audio files with the database.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleScanSounds}
|
||||||
|
disabled={scanningInProgress}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{scanningInProgress ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Scanning...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Scan className="h-4 w-4 mr-2" />
|
||||||
|
Scan Sound Directory
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{lastScanResults && (
|
||||||
|
<div className="bg-muted rounded-lg p-3 space-y-2">
|
||||||
|
<div className="text-sm font-medium">Last Scan Results:</div>
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
|
<div>✅ Added: {lastScanResults.results.added}</div>
|
||||||
|
<div>🔄 Updated: {lastScanResults.results.updated}</div>
|
||||||
|
<div>🗑️ Deleted: {lastScanResults.results.deleted}</div>
|
||||||
|
<div>⏭️ Skipped: {lastScanResults.results.skipped}</div>
|
||||||
|
{lastScanResults.results.errors.length > 0 && (
|
||||||
|
<div>❌ Errors: {lastScanResults.results.errors.length}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Sound Normalization */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<AudioWaveform className="h-5 w-5" />
|
||||||
|
Sound Normalization
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Normalize audio levels across all sounds using FFmpeg's loudnorm filter for consistent volume.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Sound Type</Label>
|
||||||
|
<Select
|
||||||
|
value={normalizationOptions.soundType}
|
||||||
|
onValueChange={(value: 'all' | 'SDB' | 'TTS' | 'EXT') =>
|
||||||
|
setNormalizationOptions(prev => ({ ...prev, soundType: value }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Sounds</SelectItem>
|
||||||
|
<SelectItem value="SDB">Soundboard (SDB)</SelectItem>
|
||||||
|
<SelectItem value="TTS">Text-to-Speech (TTS)</SelectItem>
|
||||||
|
<SelectItem value="EXT">Extracted (EXT)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="force-normalize"
|
||||||
|
checked={normalizationOptions.force}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setNormalizationOptions(prev => ({ ...prev, force: !!checked }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="force-normalize" className="text-sm">
|
||||||
|
Force re-normalization of already processed sounds
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="one-pass"
|
||||||
|
checked={normalizationOptions.onePass}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setNormalizationOptions(prev => ({ ...prev, onePass: !!checked }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="one-pass" className="text-sm">
|
||||||
|
Use one-pass normalization (faster, lower quality)
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleNormalizeSounds}
|
||||||
|
disabled={normalizationInProgress}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{normalizationInProgress ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Normalizing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Volume2 className="h-4 w-4 mr-2" />
|
||||||
|
Normalize Sounds
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{lastNormalizationResults && (
|
||||||
|
<div className="bg-muted rounded-lg p-3 space-y-2">
|
||||||
|
<div className="text-sm font-medium">Last Normalization Results:</div>
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
|
<div>🔄 Processed: {lastNormalizationResults.results.processed}</div>
|
||||||
|
<div>✅ Normalized: {lastNormalizationResults.results.normalized}</div>
|
||||||
|
<div>⏭️ Skipped: {lastNormalizationResults.results.skipped}</div>
|
||||||
|
<div>❌ Errors: {lastNormalizationResults.results.errors}</div>
|
||||||
|
{lastNormalizationResults.results.error_details.length > 0 && (
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="cursor-pointer text-red-600">View Error Details</summary>
|
||||||
|
<div className="mt-1 text-xs text-red-600 space-y-1">
|
||||||
|
{lastNormalizationResults.results.error_details.map((error, index) => (
|
||||||
|
<div key={index}>• {error}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user