import { consola } from "consola"; import { ctx } from "context"; import { eq, isNull } from "drizzle-orm"; import { decrypt } from "lib/crypto"; import { env } from "lib/env"; import _ from "lodash"; import tables from "schema"; async function getSpotifyToken(): Promise { const spotifyTokens = await ctx.db .select() .from(tables.spotifyTokens) .leftJoin( tables.spotifyAccounts, eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId), ) .leftJoin( tables.spotifyApps, eq(tables.spotifyApps.spotifyAppId, tables.spotifyTokens.spotifyAppId), ) .where(eq(tables.spotifyAccounts.isBetaUser, true)) .execute(); const record = spotifyTokens[Math.floor(Math.random() * spotifyTokens.length)]; const refreshToken = decrypt( record.spotify_tokens.refreshToken, env.SPOTIFY_ENCRYPTION_KEY, ); const accessToken = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: record.spotify_apps.spotifyAppId, client_secret: decrypt( record.spotify_apps.spotifySecret, env.SPOTIFY_ENCRYPTION_KEY, ), }), }) .then((res) => res.json() as Promise<{ access_token: string }>) .then((data) => data.access_token); return accessToken; } async function getGenresAndPicture(artists) { for (const artist of artists) { do { try { const token = await getSpotifyToken(); // search artist by name on spotify const result = await fetch( `https://api.spotify.com/v1/search?q=${encodeURIComponent(artist.name)}&type=artist&limit=1`, { headers: { Authorization: `Bearer ${token}`, }, }, ) .then( (res) => res.json() as Promise<{ artists: { items: Array<{ id: string; name: string; genres: string[]; images: Array<{ url: string }>; }>; }; }>, ) .then(async (data) => _.get(data, "artists.items.0")); if (result) { consola.info(JSON.stringify(result, null, 2), "\n"); if (result.genres && result.genres.length > 0) { await ctx.db .update(tables.artists) .set({ genres: result.genres }) .where(eq(tables.artists.id, artist.id)) .execute(); } // update artist picture if not set if (!artist.picture && result.images && result.images.length > 0) { await ctx.db .update(tables.artists) .set({ picture: result.images[0].url }) .where(eq(tables.artists.id, artist.id)) .execute(); } } break; // exit the retry loop on success } catch (error) { consola.error("Error fetching genres for artist:", artist.name, error); // wait for a while before retrying await new Promise((resolve) => setTimeout(resolve, 1000)); } // biome-ignore lint/correctness/noConstantCondition: true } while (true); // sleep for a while to avoid rate limiting await new Promise((resolve) => setTimeout(resolve, 1000)); } } const PAGE_SIZE = 1000; const count = await ctx.db .select() .from(tables.artists) .where(isNull(tables.artists.genres)) .execute() .then((res) => res.length); for (let offset = 0; offset < count; offset += PAGE_SIZE) { const artists = await ctx.db .select() .from(tables.artists) .where(isNull(tables.artists.genres)) .offset(offset) .limit(PAGE_SIZE) .execute(); await getGenresAndPicture(artists); } consola.info(`Artists without genres: ${count}`); process.exit(0);