A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
96
fork

Configure Feed

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

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);