data endpoint for entity 90008 (aka. a website)

update cover art caching to optimize the cover art size and also cache yt images

ptr.pet eaae230b b2624d81

verified
Waiting for spindle ...
+32 -20
+27 -15
eunomia/src/lib/lastfm.ts
··· 1 1 import { env } from '$env/dynamic/private'; 2 + import sharp from 'sharp'; 2 3 import { get, writable } from 'svelte/store'; 3 4 4 5 const DID = 'did:plc:dfl62fgb7wtjj3fcbb72naae'; ··· 26 27 } 27 28 }; 28 29 29 - // Fetch and cache MusicBrainz cover art 30 - const fetchAndCacheCoverArt = async (releaseMbId: string): Promise<string | null> => { 31 - const cacheFile = `${COVER_ART_CACHE_DIR}/${releaseMbId}.jpg`; 30 + // Helper to fetch, optimize, and cache an image 31 + const fetchAndCacheImage = async (url: string, id: string): Promise<string | null> => { 32 + const cacheFile = `${COVER_ART_CACHE_DIR}/${id}.webp`; 32 33 33 34 // Check if already cached 34 35 try { 35 36 await Deno.stat(cacheFile); 36 - return `/cover_art/${releaseMbId}.jpg`; 37 + return `/cover_art/${id}.webp`; 37 38 } catch { 38 39 // Not cached, try to fetch 39 40 } 40 41 41 42 try { 42 - const mbUrl = `https://coverartarchive.org/release/${releaseMbId}/front-250`; 43 - const response = await fetch(mbUrl); 43 + const response = await fetch(url); 44 44 45 45 if (!response.ok) { 46 46 return null; 47 47 } 48 48 49 49 const imageData = await response.arrayBuffer(); 50 - await Deno.writeFile(cacheFile, new Uint8Array(imageData)); 51 50 52 - return `/cover_art/${releaseMbId}.jpg`; 51 + const sharpImg = sharp(imageData).resize({ width: 92 }).webp({ quality: 80 }); 52 + const optimizedImage = await sharpImg.toBuffer(); 53 + sharpImg.destroy(); 54 + 55 + await Deno.writeFile(cacheFile, new Uint8Array(optimizedImage)); 56 + 57 + return `/cover_art/${id}.webp`; 53 58 } catch (err) { 54 - console.log(`Failed to fetch MusicBrainz cover art for ${releaseMbId}:`, err); 59 + console.log(`Failed to fetch/cache image for ${id}:`, err); 55 60 return null; 56 61 } 57 62 }; 58 63 59 - // Get YouTube thumbnail URL 60 - const getYouTubeThumbnail = (originUrl: string | null | undefined): string | null => { 64 + // Fetch and cache MusicBrainz cover art 65 + const fetchAndCacheCoverArt = async (releaseMbId: string): Promise<string | null> => { 66 + const mbUrl = `https://coverartarchive.org/release/${releaseMbId}/front-250`; 67 + return fetchAndCacheImage(mbUrl, releaseMbId); 68 + }; 69 + 70 + // Fetch and cache YouTube thumbnail 71 + const fetchAndCacheYouTubeThumbnail = async (originUrl: string | null | undefined): Promise<string | null> => { 61 72 if (!originUrl) return null; 62 73 63 74 try { ··· 68 79 videoId = originUrl.split('youtu.be/')[1]?.split('?')[0]; 69 80 } 70 81 if (videoId) { 71 - return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; 82 + const ytUrl = `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; 83 + return fetchAndCacheImage(ytUrl, videoId); 72 84 } 73 - } catch {} 85 + } catch { } 74 86 75 87 return null; 76 88 }; ··· 86 98 if (mbImage) return mbImage; 87 99 } 88 100 89 - // Fall back to YouTube thumbnail 90 - return getYouTubeThumbnail(originUrl); 101 + // Fall back to YouTube thumbnail (with caching) 102 + return fetchAndCacheYouTubeThumbnail(originUrl); 91 103 }; 92 104 93 105 export const getLastTrack = async () => {
+2 -2
eunomia/src/routes/(site)/+page.svelte
··· 178 178 ? 'object-cover' 179 179 : 'p-2'}" 180 180 style="border-style: none double none none; {data.lastTrack.image 181 - ? '' 182 - : 'image-rendering: pixelated;'}" 181 + ? 'image-rendering: smooth !important;' 182 + : 'image-rendering: pixelated !important;'}" 183 183 src={data.lastTrack.image ?? '/icons/cd_audio.webp'} 184 184 title={data.lastTrack.album} 185 185 />
+3 -3
eunomia/src/routes/cover_art/[mbid]/+server.ts
··· 2 2 import { error } from '@sveltejs/kit'; 3 3 4 4 export const GET = async ({ params }) => { 5 - const mbid = params.mbid?.replace('.jpg', ''); 5 + const mbid = params.mbid?.replace('.webp', '')?.replace('.jpg', ''); 6 6 7 7 if (!mbid) { 8 8 throw error(404, 'Missing MBID'); 9 9 } 10 10 11 11 const cacheDir = `${env.WEBSITE_DATA_DIR}/cover_art_cache`; 12 - const filePath = `${cacheDir}/${mbid}.jpg`; 12 + const filePath = `${cacheDir}/${mbid}.webp`; 13 13 14 14 try { 15 15 const file = await Deno.readFile(filePath); 16 16 return new Response(file, { 17 17 headers: { 18 - 'Content-Type': 'image/jpeg', 18 + 'Content-Type': 'image/webp', 19 19 'Cache-Control': 'public, max-age=31536000, immutable' 20 20 } 21 21 });