my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add MusicBrainz search and cover art utilities

+95
+95
src/lib/services/atproto/musicbrainz.ts
··· 1 + /** 2 + * MusicBrainz API helpers for looking up missing metadata 3 + */ 4 + 5 + import { cache } from './cache'; 6 + 7 + interface MusicBrainzRelease { 8 + id: string; 9 + score: number; 10 + title: string; 11 + 'artist-credit'?: Array<{ name: string }>; 12 + } 13 + 14 + interface MusicBrainzSearchResponse { 15 + releases: MusicBrainzRelease[]; 16 + } 17 + 18 + /** 19 + * Search MusicBrainz for a release by track name and artist 20 + * Uses conservative matching to avoid false positives 21 + */ 22 + export async function searchMusicBrainzRelease( 23 + trackName: string, 24 + artistName: string, 25 + releaseName?: string 26 + ): Promise<string | null> { 27 + const cacheKey = `mb:release:${trackName}:${artistName}:${releaseName || 'none'}`; 28 + const cached = cache.get<string | null>(cacheKey); 29 + if (cached !== null) { 30 + console.debug('[MusicBrainz] Returning cached release ID:', cached); 31 + return cached; 32 + } 33 + 34 + try { 35 + // Build search query - prefer release name if available 36 + const searchTerm = releaseName || trackName; 37 + const query = `release:"${searchTerm}" AND artist:"${artistName}"`; 38 + const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`; 39 + 40 + console.info('[MusicBrainz] Searching for:', { trackName, artistName, releaseName }); 41 + 42 + const response = await fetch(url, { 43 + headers: { 44 + 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', 45 + 'Accept': 'application/json' 46 + } 47 + }); 48 + 49 + if (!response.ok) { 50 + console.warn('[MusicBrainz] Search failed:', response.status); 51 + // Cache null result to avoid repeated failed lookups 52 + cache.set(cacheKey, null); 53 + return null; 54 + } 55 + 56 + const data: MusicBrainzSearchResponse = await response.json(); 57 + 58 + if (!data.releases || data.releases.length === 0) { 59 + console.debug('[MusicBrainz] No releases found'); 60 + cache.set(cacheKey, null); 61 + return null; 62 + } 63 + 64 + // Take the first result with a decent score (MusicBrainz uses 0-100 scale) 65 + // We want a score of at least 80 to be reasonably confident 66 + const bestMatch = data.releases[0]; 67 + if (bestMatch.score < 80) { 68 + console.debug('[MusicBrainz] Best match score too low:', bestMatch.score); 69 + cache.set(cacheKey, null); 70 + return null; 71 + } 72 + 73 + console.info('[MusicBrainz] Found release:', { 74 + id: bestMatch.id, 75 + title: bestMatch.title, 76 + artist: bestMatch['artist-credit']?.[0]?.name, 77 + score: bestMatch.score 78 + }); 79 + 80 + // Cache for 24 hours (longer than normal cache since MB IDs don't change) 81 + cache.set(cacheKey, bestMatch.id); 82 + return bestMatch.id; 83 + } catch (error) { 84 + console.error('[MusicBrainz] Search error:', error); 85 + // Don't cache errors - allow retry on next fetch 86 + return null; 87 + } 88 + } 89 + 90 + /** 91 + * Build MusicBrainz Cover Art Archive URL 92 + */ 93 + export function buildCoverArtUrl(releaseMbId: string, size: 250 | 500 | 1200 = 500): string { 94 + return `https://coverartarchive.org/release/${releaseMbId}/front-${size}`; 95 + }