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