A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/pgpull 126 lines 3.8 kB view raw
1import { ctx } from "context"; 2import { eq, isNull } from "drizzle-orm"; 3import { decrypt } from "lib/crypto"; 4import { env } from "lib/env"; 5import _ from "lodash"; 6import tables from "schema"; 7 8async function getSpotifyToken(): Promise<string> { 9 const spotifyTokens = await ctx.db 10 .select() 11 .from(tables.spotifyTokens) 12 .leftJoin( 13 tables.spotifyAccounts, 14 eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId) 15 ) 16 .where(eq(tables.spotifyAccounts.isBetaUser, true)) 17 .execute() 18 .then((res) => res.map(({ spotify_tokens }) => spotify_tokens)); 19 20 const record = 21 spotifyTokens[Math.floor(Math.random() * spotifyTokens.length)]; 22 const refreshToken = decrypt(record.refreshToken, env.SPOTIFY_ENCRYPTION_KEY); 23 24 const accessToken = await fetch("https://accounts.spotify.com/api/token", { 25 method: "POST", 26 headers: { 27 "Content-Type": "application/x-www-form-urlencoded", 28 }, 29 body: new URLSearchParams({ 30 grant_type: "refresh_token", 31 refresh_token: refreshToken, 32 client_id: env.SPOTIFY_CLIENT_ID, 33 client_secret: env.SPOTIFY_CLIENT_SECRET, 34 }), 35 }) 36 .then((res) => res.json() as Promise<{ access_token: string }>) 37 .then((data) => data.access_token); 38 39 return accessToken; 40} 41 42async function getGenresAndPicture(artists) { 43 for (const artist of artists) { 44 do { 45 try { 46 const token = await getSpotifyToken(); 47 // search artist by name on spotify 48 const result = await fetch( 49 `https://api.spotify.com/v1/search?q=${encodeURIComponent(artist.name)}&type=artist&limit=1`, 50 { 51 headers: { 52 Authorization: `Bearer ${token}`, 53 }, 54 } 55 ) 56 .then( 57 (res) => 58 res.json() as Promise<{ 59 artists: { 60 items: Array<{ 61 id: string; 62 name: string; 63 genres: string[]; 64 images: Array<{ url: string }>; 65 }>; 66 }; 67 }> 68 ) 69 .then(async (data) => _.get(data, "artists.items.0")); 70 71 if (result) { 72 console.log(JSON.stringify(result, null, 2), "\n"); 73 if (result.genres && result.genres.length > 0) { 74 await ctx.db 75 .update(tables.artists) 76 .set({ genres: result.genres }) 77 .where(eq(tables.artists.id, artist.id)) 78 .execute(); 79 } 80 // update artist picture if not set 81 if (!artist.picture && result.images && result.images.length > 0) { 82 await ctx.db 83 .update(tables.artists) 84 .set({ picture: result.images[0].url }) 85 .where(eq(tables.artists.id, artist.id)) 86 .execute(); 87 } 88 } 89 break; // exit the retry loop on success 90 } catch (error) { 91 console.error("Error fetching genres for artist:", artist.name, error); 92 // wait for a while before retrying 93 await new Promise((resolve) => setTimeout(resolve, 1000)); 94 } 95 // biome-ignore lint/correctness/noConstantCondition: true 96 } while (true); 97 98 // sleep for a while to avoid rate limiting 99 await new Promise((resolve) => setTimeout(resolve, 1000)); 100 } 101} 102 103const PAGE_SIZE = 1000; 104 105const count = await ctx.db 106 .select() 107 .from(tables.artists) 108 .where(isNull(tables.artists.genres)) 109 .execute() 110 .then((res) => res.length); 111 112for (let offset = 0; offset < count; offset += PAGE_SIZE) { 113 const artists = await ctx.db 114 .select() 115 .from(tables.artists) 116 .where(isNull(tables.artists.genres)) 117 .offset(offset) 118 .limit(PAGE_SIZE) 119 .execute(); 120 121 await getGenresAndPicture(artists); 122} 123 124console.log(`Artists without genres: ${count}`); 125 126process.exit(0);