update dependencies and add downloads page, various fixes
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { HydrusClient, extractTitleFromTags, type ServerConfig } from './api/hydrusClient'
|
||||
import { buildLibraryCacheKey, loadLibraryCache, saveLibraryCache } from './libraryCache'
|
||||
import type { MediaSection, ServerSyncSummary, Track } from './types'
|
||||
|
||||
const SYNC_SECTION_LIMIT = 2000
|
||||
const SYNC_SECTIONS: Array<{ id: Exclude<MediaSection, 'all'>; predicate: string }> = [
|
||||
{ id: 'audio', predicate: 'system:filetype = audio' },
|
||||
{ id: 'video', predicate: 'system:filetype = video' },
|
||||
{ id: 'image', predicate: 'system:filetype = image' },
|
||||
{ id: 'application', predicate: 'system:filetype = application' },
|
||||
]
|
||||
|
||||
export const LIBRARY_CACHE_SYNC_EVENT = 'api-media-player:library-cache-sync'
|
||||
|
||||
type SyncEventDetail = {
|
||||
cacheKey: string
|
||||
phase: 'started' | 'completed' | 'failed'
|
||||
serverIds: string[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type LibrarySyncServer = ServerConfig
|
||||
|
||||
export type LibraryCacheSyncResult = {
|
||||
cacheKey: string
|
||||
summaries: Record<string, ServerSyncSummary>
|
||||
addedCounts: Record<string, number>
|
||||
removedCounts: Record<string, number>
|
||||
}
|
||||
|
||||
function dispatchSyncEvent(detail: SyncEventDetail) {
|
||||
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function' || typeof CustomEvent === 'undefined') return
|
||||
window.dispatchEvent(new CustomEvent(LIBRARY_CACHE_SYNC_EVENT, { detail }))
|
||||
}
|
||||
|
||||
function 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((left, right) => right.length - left.length)[0] || null
|
||||
}
|
||||
|
||||
function buildTrackCacheKey(serverId?: string, fileId?: number) {
|
||||
return serverId && fileId != null ? `${serverId}:${fileId}` : ''
|
||||
}
|
||||
|
||||
export async function syncLibraryCache(servers: LibrarySyncServer[], options: { targetServerIds?: string[] } = {}): Promise<LibraryCacheSyncResult> {
|
||||
const cacheKey = buildLibraryCacheKey(servers)
|
||||
const targetServerIds = new Set(options.targetServerIds && options.targetServerIds.length > 0
|
||||
? options.targetServerIds
|
||||
: servers.map((server) => server.id))
|
||||
const targetServers = servers.filter((server) => targetServerIds.has(server.id))
|
||||
|
||||
if (!cacheKey || targetServers.length === 0) {
|
||||
return {
|
||||
cacheKey,
|
||||
summaries: {},
|
||||
addedCounts: {},
|
||||
removedCounts: {},
|
||||
}
|
||||
}
|
||||
|
||||
dispatchSyncEvent({ cacheKey, phase: 'started', serverIds: targetServers.map((server) => server.id) })
|
||||
|
||||
try {
|
||||
const snapshot = await loadLibraryCache(cacheKey)
|
||||
const snapshotTracks = snapshot?.tracks ?? []
|
||||
const mergedSearchCache = { ...(snapshot?.searchCache ?? {}) }
|
||||
const mergedTrackMap: Record<string, Track> = {}
|
||||
const snapshotTracksByServer: Record<string, Track[]> = {}
|
||||
let localCounter = Date.now()
|
||||
|
||||
for (const track of snapshotTracks) {
|
||||
const hydratedTrack: Track = { ...track, id: ++localCounter }
|
||||
const key = buildTrackCacheKey(hydratedTrack.serverId, hydratedTrack.fileId)
|
||||
if (!key || !hydratedTrack.serverId) continue
|
||||
|
||||
if (!snapshotTracksByServer[hydratedTrack.serverId]) {
|
||||
snapshotTracksByServer[hydratedTrack.serverId] = []
|
||||
}
|
||||
snapshotTracksByServer[hydratedTrack.serverId].push(hydratedTrack)
|
||||
|
||||
if (!targetServerIds.has(hydratedTrack.serverId)) {
|
||||
mergedTrackMap[key] = hydratedTrack
|
||||
}
|
||||
}
|
||||
|
||||
for (const searchKey of Object.keys(mergedSearchCache)) {
|
||||
if (Array.from(targetServerIds).some((serverId) => searchKey.startsWith(`${serverId}|`))) {
|
||||
delete mergedSearchCache[searchKey]
|
||||
}
|
||||
}
|
||||
|
||||
const summaries: Record<string, ServerSyncSummary> = {}
|
||||
const addedCounts: Record<string, number> = {}
|
||||
const removedCounts: Record<string, number> = {}
|
||||
|
||||
for (const server of targetServers) {
|
||||
const previousTracks = snapshotTracksByServer[server.id] ?? []
|
||||
const previousServerTrackKeys = new Set(previousTracks.map((track) => buildTrackCacheKey(track.serverId, track.fileId)).filter(Boolean))
|
||||
const currentServerTrackKeys = new Set<string>()
|
||||
const counts: ServerSyncSummary['counts'] = {}
|
||||
|
||||
try {
|
||||
const client = new HydrusClient(server)
|
||||
|
||||
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, mediaInfoMap] = await Promise.all([
|
||||
client.getFilesTags(ids, 8),
|
||||
section.id === 'application' ? client.getFilesMediaInfo(ids, 6) : Promise.resolve({} as Record<number, { mimeType?: string; isVideo?: boolean }>),
|
||||
])
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = Object.values(counts).reduce((sum, value) => sum + (value || 0), 0)
|
||||
addedCounts[server.id] = Array.from(currentServerTrackKeys).filter((key) => !previousServerTrackKeys.has(key)).length
|
||||
removedCounts[server.id] = Array.from(previousServerTrackKeys).filter((key) => !currentServerTrackKeys.has(key)).length
|
||||
summaries[server.id] = {
|
||||
updatedAt: Date.now(),
|
||||
total,
|
||||
counts,
|
||||
}
|
||||
} catch (error: any) {
|
||||
for (const track of previousTracks) {
|
||||
const key = buildTrackCacheKey(track.serverId, track.fileId)
|
||||
if (!key) continue
|
||||
mergedTrackMap[key] = track
|
||||
}
|
||||
|
||||
addedCounts[server.id] = 0
|
||||
removedCounts[server.id] = 0
|
||||
summaries[server.id] = {
|
||||
updatedAt: Date.now(),
|
||||
total: previousTracks.length,
|
||||
counts: {},
|
||||
message: `Sync failed: ${error?.message ?? String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await saveLibraryCache(cacheKey, Object.values(mergedTrackMap), mergedSearchCache)
|
||||
dispatchSyncEvent({ cacheKey, phase: 'completed', serverIds: targetServers.map((server) => server.id) })
|
||||
|
||||
return {
|
||||
cacheKey,
|
||||
summaries,
|
||||
addedCounts,
|
||||
removedCounts,
|
||||
}
|
||||
} catch (error: any) {
|
||||
const message = error?.message ?? String(error)
|
||||
dispatchSyncEvent({ cacheKey, phase: 'failed', serverIds: targetServers.map((server) => server.id), error: message })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user