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