update dependencies and add downloads page, various fixes

This commit is contained in:
2026-04-14 23:14:41 -07:00
parent 81d42662ee
commit e83648b82b
14 changed files with 1590 additions and 517 deletions
+17 -94
View File
@@ -30,18 +30,9 @@ import AddIcon from '@mui/icons-material/Add'
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
import type { Server } from '../context/ServersContext'
import { useServers } from '../context/ServersContext'
import { HydrusClient, extractTitleFromTags } from '../api/hydrusClient'
import { buildLibraryCacheKey, getLibraryCacheStats, loadLibraryCache, pruneLibraryCache, saveLibraryCache } from '../libraryCache'
import type { MediaSection, ServerSyncSummary, Track } from '../types'
const SYNC_SECTION_LIMIT = 2000
import { buildLibraryCacheKey, getLibraryCacheStats, pruneLibraryCache } from '../libraryCache'
import { syncLibraryCache } from '../librarySync'
const DEFAULT_SERVER_FORM = { name: '', host: '', port: undefined, apiKey: '', ssl: false, forceApiKeyInQuery: false }
const SYNC_SECTIONS: Array<{ id: MediaSection; label: string; predicate: string }> = [
{ id: 'audio', label: 'Audio', predicate: 'system:filetype = audio' },
{ id: 'video', label: 'Video', predicate: 'system:filetype = video' },
{ id: 'image', label: 'Image', predicate: 'system:filetype = image' },
{ id: 'application', label: 'Applications', predicate: 'system:filetype = application' },
]
type SettingsPageProps = {
onClose?: () => void
@@ -71,18 +62,6 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
const syncNoticeTimeoutsRef = useRef<Record<string, number>>({})
const currentCacheKey = useMemo(() => buildLibraryCacheKey(servers), [servers])
const extractNamespaceValue = (tags: string[] | null | undefined, ns: string) => {
if (!tags || !Array.isArray(tags)) return null
const prefix = `${ns.toLowerCase()}:`
const values = tags
.filter((tag) => typeof tag === 'string' && tag.toLowerCase().startsWith(prefix))
.map((tag) => tag.slice(prefix.length).replace(/_/g, ' ').trim())
.filter(Boolean)
return values.sort((a, b) => b.length - a.length)[0] || null
}
const buildTrackCacheKey = (serverId?: string, fileId?: number) => (serverId && fileId != null ? `${serverId}:${fileId}` : '')
useEffect(() => {
setEditing(null)
setForm(DEFAULT_SERVER_FORM)
@@ -195,82 +174,26 @@ export default function SettingsPage({ onClose, devOverlayEnabled, onDevOverlayE
setSyncingServerId(server.id)
try {
const client = new HydrusClient(server)
const cacheKey = buildLibraryCacheKey(servers)
const snapshot = await loadLibraryCache(cacheKey)
const mergedSearchCache = { ...(snapshot?.searchCache ?? {}) }
const mergedTrackMap: Record<string, Track> = {}
const previousServerTrackKeys = new Set<string>()
let localCounter = Date.now()
for (const track of snapshot?.tracks ?? []) {
const hydratedTrack: Track = { ...track, id: ++localCounter }
const key = buildTrackCacheKey(hydratedTrack.serverId, hydratedTrack.fileId)
if (!key) continue
if (hydratedTrack.serverId === server.id) {
previousServerTrackKeys.add(key)
continue
}
mergedTrackMap[key] = hydratedTrack
}
const counts: ServerSyncSummary['counts'] = {}
const currentServerTrackKeys = new Set<string>()
for (const section of SYNC_SECTIONS) {
const searchTags = [section.predicate]
const ids = await client.searchFiles(searchTags, SYNC_SECTION_LIMIT)
counts[section.id] = ids.length
mergedSearchCache[`${server.id}|${section.id}|tracks|${JSON.stringify(searchTags)}`] = ids
if (ids.length === 0) continue
const tagMap = await client.getFilesTags(ids, 8)
const mediaInfoMap = section.id === 'application' ? await client.getFilesMediaInfo(ids, 6) : {}
for (const fileId of ids) {
const tags = tagMap[fileId] || []
const key = buildTrackCacheKey(server.id, fileId)
if (!key) continue
currentServerTrackKeys.add(key)
mergedTrackMap[key] = {
id: ++localCounter,
fileId,
serverId: server.id,
serverName: server.name || server.host,
title: extractTitleFromTags(tags) || '',
artist: extractNamespaceValue(tags, 'artist') || undefined,
album: extractNamespaceValue(tags, 'album') || undefined,
tags: tags.length ? tags : undefined,
url: client.getFileUrl(fileId),
thumbnail: client.getThumbnailUrl(fileId),
mimeType: mediaInfoMap[fileId]?.mimeType,
isVideo: mediaInfoMap[fileId]?.isVideo ?? (section.id === 'video' ? true : undefined),
mediaKind: section.id,
}
}
}
await saveLibraryCache(cacheKey, Object.values(mergedTrackMap), mergedSearchCache)
const total = Object.values(counts).reduce((sum, value) => sum + (value || 0), 0)
const addedCount = Array.from(currentServerTrackKeys).filter((key) => !previousServerTrackKeys.has(key)).length
const removedCount = Array.from(previousServerTrackKeys).filter((key) => !currentServerTrackKeys.has(key)).length
const summary: ServerSyncSummary = {
updatedAt: Date.now(),
total,
counts,
}
const result = await syncLibraryCache(servers, { targetServerIds: [server.id] })
const summary = result.summaries[server.id]
if (!summary) throw new Error('Sync did not return a server summary')
updateServer(server.id, { syncSummary: summary })
if (summary.message) {
setLastTest(summary.message)
await refreshCacheStats(result.cacheKey)
return
}
const addedCount = result.addedCounts[server.id] ?? 0
const removedCount = result.removedCounts[server.id] ?? 0
const completionMessage = addedCount === 0 && removedCount === 0
? `Sync complete. No file changes. ${total} cached items.`
? `Sync complete. No file changes. ${summary.total} cached items.`
: `Sync complete. Added ${addedCount} files, removed ${removedCount} files.`
setLastTest(`Sync complete. ${total} cached items.`)
setLastTest(`Sync complete. ${summary.total} cached items.`)
setSyncCompletionNotices((current) => ({ ...current, [server.id]: completionMessage }))
await refreshCacheStats(cacheKey)
await refreshCacheStats(result.cacheKey)
if (syncNoticeTimeoutsRef.current[server.id]) {
window.clearTimeout(syncNoticeTimeoutsRef.current[server.id])
}