2026-08-29 16:38:28 +02:00
|
|
|
import { createHash } from 'node:crypto'
|
|
|
|
|
import { existsSync, mkdirSync } from 'node:fs'
|
|
|
|
|
import { writeFile } from 'node:fs/promises'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
|
|
|
|
|
const EXT_BY_TYPE: Record<string, string> = {
|
|
|
|
|
'image/jpeg': '.jpg',
|
|
|
|
|
'image/png': '.png',
|
|
|
|
|
'image/webp': '.webp',
|
|
|
|
|
'image/gif': '.gif',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Downloads `url` into `artworkDir` keyed by its sha256 hash. Returns the
|
|
|
|
|
* stored file name (e.g. `abc123….jpg`) or null when the url is empty or the
|
|
|
|
|
* download fails — artwork caching is best-effort and must never break adds.
|
|
|
|
|
*/
|
|
|
|
|
export async function cacheArtwork(
|
|
|
|
|
artworkDir: string,
|
|
|
|
|
url: string,
|
|
|
|
|
fetchImpl: typeof fetch = fetch
|
|
|
|
|
): Promise<string | null> {
|
|
|
|
|
if (!url) return null
|
|
|
|
|
const key = createHash('sha256').update(url).digest('hex')
|
|
|
|
|
try {
|
2026-08-29 16:43:45 +02:00
|
|
|
mkdirSync(artworkDir, { recursive: true })
|
|
|
|
|
for (const ext of Object.values(EXT_BY_TYPE)) {
|
|
|
|
|
if (existsSync(path.join(artworkDir, key + ext))) return key + ext
|
|
|
|
|
}
|
|
|
|
|
let res: Response
|
|
|
|
|
try {
|
|
|
|
|
res = await fetchImpl(url)
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) return null
|
|
|
|
|
const type = (res.headers.get('content-type') ?? '').split(';')[0]?.trim() ?? ''
|
|
|
|
|
const ext = EXT_BY_TYPE[type] ?? '.jpg'
|
|
|
|
|
const buf = Buffer.from(await res.arrayBuffer())
|
|
|
|
|
await writeFile(path.join(artworkDir, key + ext), buf)
|
|
|
|
|
return key + ext
|
2026-08-29 16:38:28 +02:00
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|