add thumbnail data caching

This commit is contained in:
2025-07-19 12:54:35 +02:00
parent 5cff2cdc86
commit e1644b636b

View File

@ -68,6 +68,11 @@ type Migration = {
statement: string; statement: string;
}; };
const db: sqlite3.Database = new sqlite3.cached.Database('moshingmammut.db'); const db: sqlite3.Database = new sqlite3.cached.Database('moshingmammut.db');
const maxThumbnailCacheSize = 100;
const maxThumbnailCacheAge = 1 * 60 * 60 * 1000; //1h in ms
const thumbnailCache = new Map<string, { data: SongThumbnailImage[]; ts: number }>();
let thumbnailCacheHits = 0;
let thumbnailCacheMisses = 0;
export function close() { export function close() {
try { try {
@ -803,6 +808,7 @@ async function getPostsInternal(
.flatMap((x) => x) .flatMap((x) => x)
.map((x) => x.thumbnailUrl) .map((x) => x.thumbnailUrl)
.filter((x) => x !== undefined) .filter((x) => x !== undefined)
.filter((x) => getCachedThumbnail(x) === null)
.toArray(); .toArray();
if (turls) { if (turls) {
const tMap = await getSongThumbnailData('?, '.repeat(turls.length).slice(0, -2), turls); const tMap = await getSongThumbnailData('?, '.repeat(turls.length).slice(0, -2), turls);
@ -811,7 +817,11 @@ async function getPostsInternal(
if (songInfo.thumbnailUrl === undefined) { if (songInfo.thumbnailUrl === undefined) {
continue; continue;
} }
const thumbs = tMap.get(songInfo.thumbnailUrl) ?? []; let thumbs = getCachedThumbnail(songInfo.thumbnailUrl);
if (thumbs === null) {
thumbs = tMap.get(songInfo.thumbnailUrl) ?? [];
cacheThumbnail(songInfo.thumbnailUrl, thumbs);
}
songInfo.resizedThumbnails = thumbs; songInfo.resizedThumbnails = thumbs;
} }
} }
@ -969,6 +979,73 @@ export async function getSongThumbnails(song: SongInfo): Promise<SongThumbnailIm
if (!song.thumbnailUrl) { if (!song.thumbnailUrl) {
return []; return [];
} }
const rows = await getSongThumbnailData('?', [song.thumbnailUrl]); const cachedThumbnail = getCachedThumbnail(song.thumbnailUrl);
return rows.get(song.thumbnailUrl) ?? []; if (cachedThumbnail !== null) {
return cachedThumbnail;
}
const rows = await getSongThumbnailData('?', [song.thumbnailUrl]);
const data = rows.get(song.thumbnailUrl) ?? [];
cacheThumbnail(song.thumbnailUrl, data);
return data;
}
function getCachedThumbnail(thumbnailUrl: string): SongThumbnailImage[] | null {
const cachedThumbnail = thumbnailCache.get(thumbnailUrl);
if (cachedThumbnail == undefined) {
thumbnailCacheMisses++;
return null;
}
const age = new Date().getTime() - cachedThumbnail.ts;
if (age <= maxThumbnailCacheAge) {
thumbnailCacheHits++;
return cachedThumbnail.data;
}
thumbnailCache.delete(thumbnailUrl);
thumbnailCacheMisses++;
return null;
}
function cacheThumbnail(thumbnailUrl: string, thumbnails: SongThumbnailImage[]) {
const now = new Date().getTime();
const initialSize = thumbnailCache.size;
if (initialSize >= maxThumbnailCacheSize) {
logger.debug('Sweeping cache. Current size', initialSize);
const deleteTimestampsOlderThan =
thumbnailCache
.values()
.map((x) => x.ts)
.toArray()
.sort((a, b) => a - b)
.slice(0, 20)
.pop() ?? 0;
const timestampExpired = now - maxThumbnailCacheAge;
const threshold = Math.max(timestampExpired, deleteTimestampsOlderThan);
thumbnailCache.forEach((v, k) => {
if (v.ts <= threshold) {
thumbnailCache.delete(k);
}
});
if (Logger.isDebugEnabled()) {
logger.debug(
'Swept cache. New size',
thumbnailCache.size,
'deleted',
initialSize - thumbnailCache.size,
'entries'
);
logger.debug(
'Current hitrate',
thumbnailCacheHits,
'/',
thumbnailCacheHits + thumbnailCacheMisses,
'=',
((100.0 * thumbnailCacheHits) / (thumbnailCacheHits + thumbnailCacheMisses)).toFixed(2),
'%'
);
}
}
thumbnailCache.set(thumbnailUrl, {
data: thumbnails,
ts: now
});
} }