wetalk/scripts/backfill-authors.ts
ordinarthur 0f9679ca2f
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
add good author
2026-04-13 11:45:06 +02:00

123 lines
3.9 KiB
TypeScript

/**
* 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()