forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
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);