data endpoint for entity 90008 (aka. a website)

cache coverartarchive requests

ptr.pet 6e5130b2 38bc6dce

verified
Waiting for spindle ...
+123 -54
+78 -28
eunomia/src/lib/lastfm.ts
··· 4 4 const DID = 'did:plc:dfl62fgb7wtjj3fcbb72naae'; 5 5 const PDS = 'https://zwsp.xyz'; 6 6 const LAST_TRACK_FILE = `${env.WEBSITE_DATA_DIR}/last_track.json`; 7 + const COVER_ART_CACHE_DIR = `${env.WEBSITE_DATA_DIR}/cover_art_cache`; 7 8 8 9 type LastTrack = { 9 10 name: string; 10 11 artist: string; 11 12 album: string; 12 - images: { 13 - mb: string | null; 14 - yt: string | null; 15 - }; 13 + image: string | null; // Single image URL 16 14 link: string | null; 17 15 when: number; 18 16 status: 'playing' | 'played'; 19 17 }; 20 18 const lastTrack = writable<LastTrack | null>(null); 21 19 22 - export const getLastTrack = async () => { 20 + // Ensure cache directory exists 21 + const ensureCacheDir = async () => { 23 22 try { 24 - const data = await Deno.readTextFile(LAST_TRACK_FILE); 25 - lastTrack.set(JSON.parse(data)); 26 - } catch (why) { 27 - console.log('could not read last track: ', why); 28 - lastTrack.set(null); 23 + await Deno.mkdir(COVER_ART_CACHE_DIR, { recursive: true }); 24 + } catch (err) { 25 + // Directory might already exist, ignore error 29 26 } 30 27 }; 31 28 32 - const getTrackCoverArt = ( 33 - releaseMbId: string | null | undefined, 34 - originUrl: string | null | undefined 35 - ) => { 36 - let mb: string | null = null; 37 - let yt: string | null = null; 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`; 38 32 39 - if (releaseMbId) mb = `https://coverartarchive.org/release/${releaseMbId}/front-250`; 33 + // Check if already cached 34 + try { 35 + await Deno.stat(cacheFile); 36 + return `/cover_art/${releaseMbId}.jpg`; 37 + } catch { 38 + // Not cached, try to fetch 39 + } 40 40 41 41 try { 42 - if (originUrl) { 43 - let videoId: string | null = null; 44 - if (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com')) { 45 - videoId = new URL(originUrl).searchParams.get('v'); 46 - } else if (originUrl.includes('youtu.be')) { 47 - videoId = originUrl.split('youtu.be/')[1]?.split('?')[0]; 48 - } 49 - if (videoId) yt = `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; 42 + const mbUrl = `https://coverartarchive.org/release/${releaseMbId}/front-250`; 43 + const response = await fetch(mbUrl); 44 + 45 + if (!response.ok) { 46 + return null; 47 + } 48 + 49 + const imageData = await response.arrayBuffer(); 50 + await Deno.writeFile(cacheFile, new Uint8Array(imageData)); 51 + 52 + return `/cover_art/${releaseMbId}.jpg`; 53 + } catch (err) { 54 + console.log(`Failed to fetch MusicBrainz cover art for ${releaseMbId}:`, err); 55 + return null; 56 + } 57 + }; 58 + 59 + // Get YouTube thumbnail URL 60 + const getYouTubeThumbnail = (originUrl: string | null | undefined): string | null => { 61 + if (!originUrl) return null; 62 + 63 + try { 64 + let videoId: string | null = null; 65 + if (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com')) { 66 + videoId = new URL(originUrl).searchParams.get('v'); 67 + } else if (originUrl.includes('youtu.be')) { 68 + videoId = originUrl.split('youtu.be/')[1]?.split('?')[0]; 69 + } 70 + if (videoId) { 71 + return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; 50 72 } 51 73 } catch {} 52 74 53 - return { mb, yt }; 75 + return null; 76 + }; 77 + 78 + // Get cover art with caching 79 + const getCoverArt = async ( 80 + releaseMbId: string | null | undefined, 81 + originUrl: string | null | undefined 82 + ): Promise<string | null> => { 83 + // Try MusicBrainz first (with caching) 84 + if (releaseMbId) { 85 + const mbImage = await fetchAndCacheCoverArt(releaseMbId); 86 + if (mbImage) return mbImage; 87 + } 88 + 89 + // Fall back to YouTube thumbnail 90 + return getYouTubeThumbnail(originUrl); 91 + }; 92 + 93 + export const getLastTrack = async () => { 94 + try { 95 + const data = await Deno.readTextFile(LAST_TRACK_FILE); 96 + lastTrack.set(JSON.parse(data)); 97 + } catch (why) { 98 + console.log('could not read last track: ', why); 99 + lastTrack.set(null); 100 + } 54 101 }; 55 102 56 103 const joinArtists = (artists: any[]) => { 57 104 if (!artists || artists.length === 0) return null; 58 - // remove duplicates 59 105 const uniqueArtists = [...new Set(artists.map((a) => a.artistName))]; 60 106 return uniqueArtists.join(', '); 61 107 }; 62 108 63 109 export const updateNowPlayingTrack = async () => { 110 + await ensureCacheDir(); 111 + 64 112 try { 65 113 let track: any = null; 66 114 let when: number = Date.now(); ··· 86 134 87 135 if (!track) return; 88 136 137 + const coverArt = await getCoverArt(track.releaseMbId, track.originUrl); 138 + 89 139 const data: LastTrack = { 90 140 name: track.trackName, 91 141 artist: joinArtists(track.artists) ?? 'Unknown Artist', 92 142 album: track.releaseName ?? 'Unknown Album', 93 - images: getTrackCoverArt(track.releaseMbId, track.originUrl), 143 + image: coverArt, 94 144 link: track.originUrl ?? null, 95 145 when: when, 96 146 status: status
+15 -10
eunomia/src/lib/robots.ts
··· 11 11 const lastFetched = writable<number>(Date.now()); 12 12 13 13 const fetchRobotsTxt = async () => { 14 - const robotsTxt = await darkVisitors.generateRobotsTxt([ 15 - AgentType.AIAgent, 16 - AgentType.AIAssistant, 17 - AgentType.AIDataScraper, 18 - AgentType.AISearchCrawler, 19 - AgentType.UndocumentedAIAgent, 20 - AgentType.SEOCrawler 21 - ]); 22 - lastFetched.set(Date.now()); 23 - return robotsTxt; 14 + try { 15 + const robotsTxt = await darkVisitors.generateRobotsTxt([ 16 + AgentType.AIAgent, 17 + AgentType.AIAssistant, 18 + AgentType.AIDataScraper, 19 + AgentType.AISearchCrawler, 20 + AgentType.UndocumentedAIAgent, 21 + AgentType.SEOCrawler 22 + ]); 23 + lastFetched.set(Date.now()); 24 + return robotsTxt; 25 + } catch (error) { 26 + console.error('failed to fetch robots.txt:', error); 27 + return ''; 28 + } 24 29 }; 25 30 26 31 export const getRobotsTxt = async () => {
+5 -16
eunomia/src/routes/(site)/+page.svelte
··· 168 168 </div> 169 169 {/if} 170 170 {#if data.lastTrack} 171 - {@const images = data.lastTrack.images} 172 - {@const initialUrl = images.mb ?? images.yt} 173 171 {@const showAlbum = 174 172 data.lastTrack.album && 175 173 data.lastTrack.name.toLowerCase() !== data.lastTrack.album.toLowerCase()} ··· 178 176 <img 179 177 class="border-4 {showAlbum 180 178 ? 'w-[5.75rem] h-[5.75rem]' 181 - : 'w-[4.5rem] h-[4.5rem]'} {initialUrl ? 'object-cover' : 'p-2'}" 182 - style="border-style: none double none none; {initialUrl 179 + : 'w-[4.5rem] h-[4.5rem]'} {data.lastTrack.image 180 + ? 'object-cover' 181 + : 'p-2'}" 182 + style="border-style: none double none none; {data.lastTrack.image 183 183 ? '' 184 184 : 'image-rendering: pixelated;'}" 185 - src={initialUrl ?? '/icons/cd_audio.webp'} 185 + src={data.lastTrack.image ?? '/icons/cd_audio.webp'} 186 186 title={data.lastTrack.album} 187 - onerror={(e) => { 188 - const img = e.currentTarget as HTMLImageElement; 189 - if (images.mb && img.src === images.mb && images.yt) 190 - img.src = images.yt; 191 - else { 192 - img.src = '/icons/cd_audio.webp'; 193 - img.classList.remove('object-cover'); 194 - img.classList.add('p-2'); 195 - img.style.imageRendering = 'pixelated'; 196 - } 197 - }} 198 187 /> 199 188 <div class="flex flex-col max-w-[60ch] p-2 text-ellipsis overflow-hidden"> 200 189 <p
+25
eunomia/src/routes/cover_art/[mbid]/+server.ts
··· 1 + import { env } from '$env/dynamic/private'; 2 + import { error } from '@sveltejs/kit'; 3 + 4 + export const GET = async ({ params }) => { 5 + const mbid = params.mbid?.replace('.jpg', ''); 6 + 7 + if (!mbid) { 8 + throw error(404, 'Missing MBID'); 9 + } 10 + 11 + const cacheDir = `${env.WEBSITE_DATA_DIR}/cover_art_cache`; 12 + const filePath = `${cacheDir}/${mbid}.jpg`; 13 + 14 + try { 15 + const file = await Deno.readFile(filePath); 16 + return new Response(file, { 17 + headers: { 18 + 'Content-Type': 'image/jpeg', 19 + 'Cache-Control': 'public, max-age=31536000, immutable' 20 + } 21 + }); 22 + } catch { 23 + throw error(404, 'cover art not found'); 24 + } 25 + };