mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
优化缩略图
This commit is contained in:
Vendored
+74
-1
@@ -1,8 +1,22 @@
|
||||
const ARTWORK_CACHE_PREFIX = 'mebox-artwork-'
|
||||
const ARTWORK_CACHE = `${ARTWORK_CACHE_PREFIX}v2`
|
||||
const ARTWORK_CACHE = `${ARTWORK_CACHE_PREFIX}v3`
|
||||
const MIN_CACHEABLE_ARTWORK_BYTES = 128
|
||||
const STRIP_QUERY_KEYS = ['token', 'profile_id', 'profile_pin_token']
|
||||
|
||||
// 浏览器侧作品缓存的容量上限。
|
||||
//
|
||||
// 没有上限时 Cache Storage 会一直增长,浏览器在存储压力下会整体清空该
|
||||
// origin 的缓存,失效时机完全不可控。条数上限每次写入都检查(只是读一遍
|
||||
// 键,开销小);字节上限最多每两分钟统计一次,因为需要逐个读取已存响应的
|
||||
// Content-Length。两者都淘汰到上限的 80%,避免刚清完又立刻触发。
|
||||
const MAX_CACHE_ENTRIES = 500
|
||||
const MAX_CACHE_BYTES = 64 * 1024 * 1024
|
||||
const TRIM_TARGET_RATIO = 0.8
|
||||
const BYTE_TRIM_INTERVAL_MS = 2 * 60 * 1000
|
||||
|
||||
let lastByteTrimAt = 0
|
||||
let trimInFlight = null
|
||||
|
||||
function isArtworkRequest(url) {
|
||||
if (url.origin !== self.location.origin) return false
|
||||
if (url.pathname === '/api/img') return true
|
||||
@@ -54,10 +68,69 @@ async function cacheArtwork(request) {
|
||||
if (cacheResponse) {
|
||||
await cache.put(cacheKey, cacheResponse)
|
||||
await deleteOldArtworkVariants(cache, cacheKey)
|
||||
await trimArtworkCache(cache)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// trimArtworkCache 把缓存裁剪回容量上限之内,按写入顺序淘汰(最旧的先删)。
|
||||
// 同一时刻只允许一次裁剪在跑。
|
||||
async function trimArtworkCache(cache) {
|
||||
if (trimInFlight) return trimInFlight
|
||||
trimInFlight = runArtworkTrim(cache).catch(() => undefined)
|
||||
try {
|
||||
await trimInFlight
|
||||
} finally {
|
||||
trimInFlight = null
|
||||
}
|
||||
}
|
||||
|
||||
async function runArtworkTrim(cache) {
|
||||
const keys = await cache.keys()
|
||||
if (keys.length > MAX_CACHE_ENTRIES) {
|
||||
const keep = Math.floor(MAX_CACHE_ENTRIES * TRIM_TARGET_RATIO)
|
||||
await deleteOldestArtwork(cache, keys, keys.length - keep)
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
if (now - lastByteTrimAt < BYTE_TRIM_INTERVAL_MS) return
|
||||
lastByteTrimAt = now
|
||||
|
||||
const sizes = []
|
||||
let total = 0
|
||||
for (const key of keys) {
|
||||
const response = await cache.match(key)
|
||||
const size = cacheableResponseSize(response)
|
||||
sizes.push(size)
|
||||
total += size
|
||||
}
|
||||
if (total <= MAX_CACHE_BYTES) return
|
||||
|
||||
const target = Math.floor(MAX_CACHE_BYTES * TRIM_TARGET_RATIO)
|
||||
let remaining = total
|
||||
const evicted = []
|
||||
for (let i = 0; i < keys.length && remaining > target; i += 1) {
|
||||
remaining -= sizes[i]
|
||||
evicted.push(keys[i])
|
||||
}
|
||||
await Promise.all(evicted.map((key) => cache.delete(key)))
|
||||
}
|
||||
|
||||
// cacheableResponseSize 用 Content-Length 估算体积。MeBox 的图片响应由
|
||||
// ServeContent 生成,始终带该头;缺失时按 0 计(只是少算,不会误删)。
|
||||
function cacheableResponseSize(response) {
|
||||
if (!response) return 0
|
||||
const size = Number(response.headers.get('Content-Length') || '0')
|
||||
return Number.isFinite(size) && size > 0 ? size : 0
|
||||
}
|
||||
|
||||
// deleteOldestArtwork 按 cache.keys() 的顺序(写入顺序)删除最旧的若干条。
|
||||
async function deleteOldestArtwork(cache, keys, count) {
|
||||
const victims = keys.slice(0, Math.max(0, count))
|
||||
await Promise.all(victims.map((key) => cache.delete(key)))
|
||||
}
|
||||
|
||||
async function cloneCacheableArtworkResponse(response) {
|
||||
if (!response.ok) return null
|
||||
const contentType = response.headers.get('Content-Type') || ''
|
||||
|
||||
@@ -111,10 +111,26 @@ export const generalSettingsGroup: SettingGroup = {
|
||||
key: 'cache.images_max_size_mb',
|
||||
label: '图片缓存上限 (MB)',
|
||||
type: 'number',
|
||||
hint: '海报、剧照等图片代理缓存目录 (cache/images) 的最大占用空间。超过上限时自动按修改时间清理最旧的文件;上限过小会导致旧图被淘汰后又要重新从上游/挂载的 Emby 下载。设为 0 表示不限制,默认 2000MB',
|
||||
hint: '海报、剧照等图片代理缓存目录 (cache/images) 的总占用上限。超过上限时先淘汰原图、再按修改时间清理最旧的缩略图;上限过小会导致旧图被淘汰后又要重新从上游/挂载的 Emby 下载。设为 0 表示不限制,默认 2000MB',
|
||||
defaultValue: '2000',
|
||||
placeholder: '2000',
|
||||
},
|
||||
{
|
||||
key: 'cache.images_originals_max_size_mb',
|
||||
label: '原图缓存上限 (MB)',
|
||||
type: 'number',
|
||||
hint: '原图只是生成缩略图的原料,丢了可以重新回源,因此单独给一个小配额(默认 256MB)。总量超限时原图也优先被淘汰,保证客户端实际读取的缩略图能留住。设为 0 表示不单独限制',
|
||||
defaultValue: '256',
|
||||
placeholder: '256',
|
||||
},
|
||||
{
|
||||
key: 'cache.images_originals_ttl_hours',
|
||||
label: '原图保留时长 (小时)',
|
||||
type: 'number',
|
||||
hint: '原图缓存的最长保留时间,超过即删除(默认 168 小时 = 7 天)。缩略图都已生成时原图就不再需要,缩短它可以让原图缓存保持在一个滚动窗口内。设为 0 表示不按时间淘汰',
|
||||
defaultValue: '168',
|
||||
placeholder: '168',
|
||||
},
|
||||
{
|
||||
key: 'https.enabled',
|
||||
label: '启用 HTTPS',
|
||||
|
||||
Reference in New Issue
Block a user