Something went wrong. Try again.
A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137import { 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<string> { 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( `${env.SPOTIFY_API_URL}/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);