update library sync and client to support new hydrus api
This commit is contained in:
+15
-4
@@ -32,6 +32,17 @@ export type HydrusFileDetails = HydrusMediaInfo & {
|
|||||||
tags: string[]
|
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() {
|
export function makeId() {
|
||||||
return Date.now().toString() + '-' + Math.random().toString(36).slice(2, 9)
|
return Date.now().toString() + '-' + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
@@ -54,10 +65,10 @@ export class HydrusClient {
|
|||||||
constructor(cfg: Partial<ServerConfig> = {}) {
|
constructor(cfg: Partial<ServerConfig> = {}) {
|
||||||
this.cfg = {
|
this.cfg = {
|
||||||
id: (cfg.id as string) || makeId(),
|
id: (cfg.id as string) || makeId(),
|
||||||
name: cfg.name || '',
|
name: (cfg.name || '').trim(),
|
||||||
host: cfg.host || '',
|
host: (cfg.host || '').trim(),
|
||||||
port: cfg.port,
|
port: sanitizePort(cfg.port),
|
||||||
apiKey: cfg.apiKey,
|
apiKey: sanitizeApiKey(cfg.apiKey),
|
||||||
ssl: !!cfg.ssl,
|
ssl: !!cfg.ssl,
|
||||||
forceApiKeyInQuery: !!cfg.forceApiKeyInQuery
|
forceApiKeyInQuery: !!cfg.forceApiKeyInQuery
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,56 @@ import { syncLibraryCache } from '../librarySync'
|
|||||||
import { APP_THEME_PRESETS, type AppThemeId } from '../themes'
|
import { APP_THEME_PRESETS, type AppThemeId } from '../themes'
|
||||||
const DEFAULT_SERVER_FORM = { name: '', host: '', port: undefined, apiKey: '', ssl: false, forceApiKeyInQuery: false }
|
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 = {
|
type SettingsPageProps = {
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
devOverlayEnabled: boolean
|
devOverlayEnabled: boolean
|
||||||
@@ -139,6 +189,7 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
|
|||||||
const startAdd = () => {
|
const startAdd = () => {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
setForm(DEFAULT_SERVER_FORM)
|
setForm(DEFAULT_SERVER_FORM)
|
||||||
|
setLastTest(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startEdit = (s: Server) => {
|
const startEdit = (s: Server) => {
|
||||||
@@ -148,10 +199,17 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
const { normalized, error } = validateServerForm(form)
|
||||||
|
if (error) {
|
||||||
|
setLastTest(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setForm(normalized)
|
||||||
if (editing) {
|
if (editing) {
|
||||||
updateServer(editing.id, form)
|
updateServer(editing.id, normalized)
|
||||||
} else {
|
} else {
|
||||||
const srv = addServer(form)
|
const srv = addServer(normalized)
|
||||||
setActiveServerId(srv.id)
|
setActiveServerId(srv.id)
|
||||||
}
|
}
|
||||||
onClose && onClose()
|
onClose && onClose()
|
||||||
@@ -176,9 +234,16 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleTestForm = async () => {
|
const handleTestForm = async () => {
|
||||||
|
const { normalized, error } = validateServerForm(form)
|
||||||
|
if (error) {
|
||||||
|
setLastTest(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setForm(normalized)
|
||||||
setTesting(true)
|
setTesting(true)
|
||||||
try {
|
try {
|
||||||
const res = await testServerConfig(form)
|
const res = await testServerConfig(normalized)
|
||||||
setLastTest(`${res.message}`)
|
setLastTest(`${res.message}`)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setLastTest(`Error: ${e?.message ?? String(e)}`)
|
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 }}>
|
<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="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="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 />
|
<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 />
|
<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.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" />
|
<FormControlLabel control={<Switch checked={!!form.forceApiKeyInQuery} onChange={(e) => setForm({ ...form, forceApiKeyInQuery: e.target.checked })} />} label="Send API key in query parameter" />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user