/** * Backfill external_author and external_author_avatar for existing external podcasts. * * Usage: * cd podcast-us * bun run scripts/backfill-authors.ts * * Requires: * - API server running on localhost:3001 (cd api && bun run dev) * - .env with VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY */ const SUPABASE_URL = process.env.VITE_SUPABASE_URL || '' const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.VITE_SUPABASE_ANON_KEY || '' const API_URL = process.env.VITE_API_URL || 'http://localhost:3001' if (!SUPABASE_URL || !SUPABASE_KEY) { console.error('Missing VITE_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env') process.exit(1) } if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { console.warn('⚠ No SUPABASE_SERVICE_ROLE_KEY found — using anon key. Updates may fail due to RLS.') console.warn(' Set SUPABASE_SERVICE_ROLE_KEY=... in .env or pass it as env var.\n') } // Patterns that indicate an external URL (not a Supabase storage URL) function isExternalUrl(url: string): boolean { return ( url.includes('youtube.com') || url.includes('youtu.be') || url.includes('spotify.com') || url.includes('dailymotion.com') || url.includes('dai.ly') || url.includes('soundcloud.com') ) } async function supabaseFetch(path: string, options?: RequestInit) { const res = await fetch(`${SUPABASE_URL}/rest/v1${path}`, { ...options, headers: { apikey: SUPABASE_KEY, Authorization: `Bearer ${SUPABASE_KEY}`, 'Content-Type': 'application/json', Prefer: options?.method === 'PATCH' ? 'return=minimal' : 'return=representation', ...options?.headers, }, }) return res } async function main() { // 1. Fetch all podcasts const res = await supabaseFetch('/podcasts?select=id,audio_url,external_author,external_author_avatar&order=created_at.desc') if (!res.ok) { console.error('Failed to fetch podcasts:', await res.text()) process.exit(1) } const podcasts: { id: string; audio_url: string; external_author: string | null; external_author_avatar: string | null }[] = await res.json() // 2. Filter external podcasts that need backfill const toUpdate = podcasts.filter(p => isExternalUrl(p.audio_url) && (!p.external_author || !p.external_author_avatar)) console.log(`Found ${podcasts.length} total podcasts, ${toUpdate.length} external podcasts to backfill\n`) if (toUpdate.length === 0) { console.log('Nothing to do!') return } let success = 0 let failed = 0 for (const podcast of toUpdate) { try { // 3. Fetch metadata from API const metaRes = await fetch(`${API_URL}/api/metadata?url=${encodeURIComponent(podcast.audio_url)}`) if (!metaRes.ok) { console.log(` SKIP ${podcast.id} — API returned ${metaRes.status} for ${podcast.audio_url}`) failed++ continue } const meta = await metaRes.json() if (!meta.author && !meta.authorAvatar) { console.log(` SKIP ${podcast.id} — no author data from API`) failed++ continue } // 4. Update podcast const updateRes = await supabaseFetch(`/podcasts?id=eq.${podcast.id}`, { method: 'PATCH', body: JSON.stringify({ external_author: meta.author || null, external_author_avatar: meta.authorAvatar || null, }), }) if (updateRes.ok) { console.log(` OK ${podcast.id} — ${meta.author} ${meta.authorAvatar ? '(+avatar)' : '(no avatar)'}`) success++ } else { console.log(` FAIL ${podcast.id} — DB update failed: ${await updateRes.text()}`) failed++ } // Rate limit: small delay between requests await new Promise(r => setTimeout(r, 200)) } catch (err) { console.log(` ERR ${podcast.id} — ${err}`) failed++ } } console.log(`\nDone! ${success} updated, ${failed} failed/skipped`) } main()