update library sync and client to support new hydrus api

This commit is contained in:
2026-04-23 13:38:16 -07:00
parent 886db2d904
commit d58686bce9
2 changed files with 86 additions and 10 deletions
+15 -4
View File
@@ -32,6 +32,17 @@ export type HydrusFileDetails = HydrusMediaInfo & {
tags: string[]
}
function sanitizeApiKey(value?: string) {
return (value || '').replace(/\s+/g, '')
}
function sanitizePort(value?: string | number) {
if (value == null) return undefined
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
const trimmed = value.trim()
return trimmed ? trimmed : undefined
}
export function makeId() {
return Date.now().toString() + '-' + Math.random().toString(36).slice(2, 9)
}
@@ -54,10 +65,10 @@ export class HydrusClient {
constructor(cfg: Partial<ServerConfig> = {}) {
this.cfg = {
id: (cfg.id as string) || makeId(),
name: cfg.name || '',
host: cfg.host || '',
port: cfg.port,
apiKey: cfg.apiKey,
name: (cfg.name || '').trim(),
host: (cfg.host || '').trim(),
port: sanitizePort(cfg.port),
apiKey: sanitizeApiKey(cfg.apiKey),
ssl: !!cfg.ssl,
forceApiKeyInQuery: !!cfg.forceApiKeyInQuery
}
+71 -6
View File
@@ -36,6 +36,56 @@ import { syncLibraryCache } from '../librarySync'
import { APP_THEME_PRESETS, type AppThemeId } from '../themes'
const DEFAULT_SERVER_FORM = { name: '', host: '', port: undefined, apiKey: '', ssl: false, forceApiKeyInQuery: false }
function normalizeServerForm(form: Omit<Server, 'id' | 'lastTest'>) {
return {
...form,
name: (form.name || '').trim(),
host: (form.host || '').trim(),
port: typeof form.port === 'string' ? form.port.trim() || undefined : form.port,
apiKey: (form.apiKey || '').replace(/\s+/g, ''),
}
}
function validateServerForm(form: Omit<Server, 'id' | 'lastTest'>) {
const normalized = normalizeServerForm(form)
if (!normalized.host) {
return { normalized, error: 'Host is required.' }
}
try {
let candidate = normalized.host
if (!/^https?:\/\//i.test(candidate)) {
candidate = `${normalized.ssl ? 'https' : 'http'}://${candidate}`
}
const parsed = new URL(candidate)
if (!parsed.hostname) {
return { normalized, error: 'Host is invalid. Use an IP, hostname, or full URL.' }
}
} catch {
return { normalized, error: 'Host is invalid. Use an IP, hostname, or full URL.' }
}
if (normalized.port !== undefined) {
const portText = String(normalized.port)
if (!/^\d+$/.test(portText)) {
return { normalized, error: 'Port must contain digits only.' }
}
const portNumber = Number(portText)
if (portNumber < 1 || portNumber > 65535) {
return { normalized, error: 'Port must be between 1 and 65535.' }
}
}
if (normalized.apiKey && !/^[a-f0-9]{64}$/i.test(normalized.apiKey)) {
return { normalized, error: 'API key should be a 64-character hexadecimal key. Any pasted spaces or line breaks were removed automatically.' }
}
return { normalized, error: null }
}
type SettingsPageProps = {
onClose?: () => void
devOverlayEnabled: boolean
@@ -139,6 +189,7 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
const startAdd = () => {
setEditing(null)
setForm(DEFAULT_SERVER_FORM)
setLastTest(null)
}
const startEdit = (s: Server) => {
@@ -148,10 +199,17 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
}
const handleSave = () => {
const { normalized, error } = validateServerForm(form)
if (error) {
setLastTest(error)
return
}
setForm(normalized)
if (editing) {
updateServer(editing.id, form)
updateServer(editing.id, normalized)
} else {
const srv = addServer(form)
const srv = addServer(normalized)
setActiveServerId(srv.id)
}
onClose && onClose()
@@ -176,9 +234,16 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
}
const handleTestForm = async () => {
const { normalized, error } = validateServerForm(form)
if (error) {
setLastTest(error)
return
}
setForm(normalized)
setTesting(true)
try {
const res = await testServerConfig(form)
const res = await testServerConfig(normalized)
setLastTest(`${res.message}`)
} catch (e: any) {
setLastTest(`Error: ${e?.message ?? String(e)}`)
@@ -453,9 +518,9 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
)}
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField label="Name (optional)" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} fullWidth />
<TextField label="Host or IP" placeholder="192.168.1.128 or hydrus.local" value={form.host} onChange={(e) => setForm({ ...form, host: e.target.value })} fullWidth />
<TextField label="Port" placeholder="45869" value={form.port as any ?? ''} onChange={(e) => setForm({ ...form, port: e.target.value })} fullWidth />
<TextField label="API Key (optional)" value={form.apiKey} onChange={(e) => setForm({ ...form, apiKey: e.target.value })} fullWidth />
<TextField label="Host or URL" placeholder="192.168.1.128, hydrus.local, or http://192.168.1.128" value={form.host} onChange={(e) => setForm({ ...form, host: e.target.value })} fullWidth autoCapitalize="off" autoCorrect="off" spellCheck={false} />
<TextField label="Port" placeholder="45869" value={form.port as any ?? ''} onChange={(e) => setForm({ ...form, port: e.target.value })} fullWidth inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }} />
<TextField label="API Key (optional)" value={form.apiKey} onChange={(e) => setForm({ ...form, apiKey: e.target.value })} fullWidth autoCapitalize="off" autoCorrect="off" spellCheck={false} helperText="Hex API key. Pasted spaces or line breaks are ignored." />
<FormControlLabel control={<Switch checked={!!form.ssl} onChange={(e) => setForm({ ...form, ssl: e.target.checked })} />} label="Use HTTPS (SSL)" />
<FormControlLabel control={<Switch checked={!!form.forceApiKeyInQuery} onChange={(e) => setForm({ ...form, forceApiKeyInQuery: e.target.checked })} />} label="Send API key in query parameter" />