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

Configure Feed

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

refactor: update crypto imports to use 'node:crypto' and improve code formatting

+93 -93
+3 -3
apps/api/src/lib/crypto.ts
··· 1 - import crypto from "crypto"; 1 + import crypto from "node:crypto"; 2 2 import { env } from "./env"; 3 3 4 4 export function encrypt(text: string, key: string) { ··· 6 6 const cipher = crypto.createCipheriv( 7 7 "aes-256-ctr", 8 8 Buffer.from(key, "hex"), 9 - iv, 9 + iv 10 10 ); 11 11 const encrypted = Buffer.concat([ 12 12 cipher.update(text, "utf8"), ··· 21 21 const decipher = crypto.createDecipheriv( 22 22 "aes-256-ctr", 23 23 Buffer.from(key, "hex"), 24 - iv, 24 + iv 25 25 ); 26 26 const decrypted = Buffer.concat([decipher.update(content), decipher.final()]); 27 27 return decrypted.toString("utf8");
+18 -18
apps/api/src/lovedtracks/lovedtracks.service.ts
··· 2 2 import { TID } from "@atproto/common"; 3 3 import { equals } from "@xata.io/client"; 4 4 import type { Context } from "context"; 5 - import { createHash } from "crypto"; 6 5 import * as LikeLexicon from "lexicon/types/app/rocksky/like"; 7 6 import { validateMain } from "lexicon/types/com/atproto/repo/strongRef"; 7 + import { createHash } from "node:crypto"; 8 8 import type { Track } from "types/track"; 9 9 10 10 export async function likeTrack( 11 11 ctx: Context, 12 12 track: Track, 13 13 user, 14 - agent: Agent, 14 + agent: Agent 15 15 ) { 16 16 const existingTrack = await ctx.client.db.tracks 17 17 .filter( ··· 19 19 equals( 20 20 createHash("sha256") 21 21 .update( 22 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 22 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 23 23 ) 24 - .digest("hex"), 25 - ), 24 + .digest("hex") 25 + ) 26 26 ) 27 27 .getFirst(); 28 28 ··· 43 43 // compute sha256 (lowercase(title + artist + album)) 44 44 sha256: createHash("sha256") 45 45 .update( 46 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 46 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 47 47 ) 48 48 .digest("hex"), 49 - }, 49 + } 50 50 ); 51 51 52 52 const existingArtist = await ctx.client.db.artists ··· 55 55 equals( 56 56 createHash("sha256") 57 57 .update(track.albumArtist.toLocaleLowerCase()) 58 - .digest("hex"), 59 - ), 58 + .digest("hex") 59 + ) 60 60 ) 61 61 .getFirst(); 62 62 const { xata_id: artist_id } = await ctx.client.db.artists.createOrUpdate( ··· 67 67 sha256: createHash("sha256") 68 68 .update(track.albumArtist.toLowerCase()) 69 69 .digest("hex"), 70 - }, 70 + } 71 71 ); 72 72 73 73 const existingAlbum = await ctx.client.db.albums ··· 76 76 equals( 77 77 createHash("sha256") 78 78 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 79 - .digest("hex"), 80 - ), 79 + .digest("hex") 80 + ) 81 81 ) 82 82 .getFirst(); 83 83 ··· 95 95 sha256: createHash("sha256") 96 96 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 97 97 .digest("hex"), 98 - }, 98 + } 99 99 ); 100 100 101 101 const existingAlbumTrack = await ctx.client.db.album_tracks ··· 118 118 { 119 119 artist_id, 120 120 track_id, 121 - }, 121 + } 122 122 ); 123 123 124 124 const existingArtistAlbum = await ctx.client.db.artist_albums ··· 131 131 { 132 132 artist_id, 133 133 album_id, 134 - }, 134 + } 135 135 ); 136 136 137 137 const lovedTrack = await ctx.client.db.loved_tracks ··· 144 144 { 145 145 user_id: user.xata_id, 146 146 track_id, 147 - }, 147 + } 148 148 ); 149 149 150 150 if (existingTrack.uri) { ··· 206 206 ctx: Context, 207 207 trackSha256: string, 208 208 user, 209 - agent: Agent, 209 + agent: Agent 210 210 ) { 211 211 const track = await ctx.client.db.tracks 212 212 .filter("sha256", equals(trackSha256)) ··· 244 244 ctx: Context, 245 245 user, 246 246 size = 10, 247 - offset = 0, 247 + offset = 0 248 248 ) { 249 249 const lovedTracks = await ctx.client.db.loved_tracks 250 250 .select(["track_id.*"])
+33 -33
apps/api/src/nowplaying/nowplaying.service.ts
··· 3 3 import { equals } from "@xata.io/client"; 4 4 import chalk from "chalk"; 5 5 import type { Context } from "context"; 6 - import { createHash } from "crypto"; 7 6 import dayjs from "dayjs"; 8 7 import * as Album from "lexicon/types/app/rocksky/album"; 9 8 import * as Artist from "lexicon/types/app/rocksky/artist"; 10 9 import * as Scrobble from "lexicon/types/app/rocksky/scrobble"; 11 10 import * as Song from "lexicon/types/app/rocksky/song"; 12 11 import downloadImage, { getContentType } from "lib/downloadImage"; 12 + import { createHash } from "node:crypto"; 13 13 import type { Track } from "types/track"; 14 14 15 15 export async function putArtistRecord( 16 16 track: Track, 17 - agent: Agent, 17 + agent: Agent 18 18 ): Promise<string | null> { 19 19 const rkey = TID.nextStr(); 20 20 const record: { ··· 63 63 64 64 export async function putAlbumRecord( 65 65 track: Track, 66 - agent: Agent, 66 + agent: Agent 67 67 ): Promise<string | null> { 68 68 const rkey = TID.nextStr(); 69 69 let albumArt; ··· 123 123 124 124 export async function putSongRecord( 125 125 track: Track, 126 - agent: Agent, 126 + agent: Agent 127 127 ): Promise<string | null> { 128 128 const rkey = TID.nextStr(); 129 129 let albumArt; ··· 191 191 192 192 async function putScrobbleRecord( 193 193 track: Track, 194 - agent: Agent, 194 + agent: Agent 195 195 ): Promise<string | null> { 196 196 const rkey = TID.nextStr(); 197 197 let albumArt; ··· 369 369 ctx: Context, 370 370 track: Track, 371 371 agent: Agent, 372 - userDid: string, 372 + userDid: string 373 373 ): Promise<void> { 374 374 // check if scrobble already exists (user did + timestamp) 375 375 const scrobbleTime = dayjs.unix(track.timestamp); ··· 393 393 if (existingScrobble) { 394 394 console.log( 395 395 `Scrobble already exists for ${chalk.cyan(track.title)} at ${chalk.cyan( 396 - dayjs.unix(track.timestamp).format("YYYY-MM-DD HH:mm:ss"), 397 - )}`, 396 + dayjs.unix(track.timestamp).format("YYYY-MM-DD HH:mm:ss") 397 + )}` 398 398 ); 399 399 return; 400 400 } ··· 406 406 equals( 407 407 createHash("sha256") 408 408 .update( 409 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 409 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 410 410 ) 411 - .digest("hex"), 412 - ), 411 + .digest("hex") 412 + ) 413 413 ) 414 414 .getFirst(); 415 415 ··· 420 420 equals( 421 421 createHash("sha256") 422 422 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 423 - .digest("hex"), 424 - ), 423 + .digest("hex") 424 + ) 425 425 ) 426 426 .getFirst(); 427 427 if (album) { ··· 438 438 equals( 439 439 createHash("sha256") 440 440 .update(track.albumArtist.toLowerCase()) 441 - .digest("hex"), 442 - ), 441 + .digest("hex") 442 + ) 443 443 ) 444 444 .getFirst(); 445 445 if (artist) { ··· 466 466 equals( 467 467 createHash("sha256") 468 468 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 469 - .digest("hex"), 470 - ), 469 + .digest("hex") 470 + ) 471 471 ) 472 472 .getFirst(); 473 473 ··· 480 480 equals( 481 481 createHash("sha256") 482 482 .update( 483 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 483 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 484 484 ) 485 - .digest("hex"), 486 - ), 485 + .digest("hex") 486 + ) 487 487 ) 488 488 .getFirst(); 489 489 await new Promise((resolve) => setTimeout(resolve, 1000)); ··· 496 496 497 497 if (existingTrack) { 498 498 console.log( 499 - `Song found: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, 499 + `Song found: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 500 500 ); 501 501 } 502 502 ··· 546 546 equals( 547 547 createHash("sha256") 548 548 .update( 549 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 549 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 550 550 ) 551 - .digest("hex"), 552 - ), 551 + .digest("hex") 552 + ) 553 553 ) 554 554 .getFirst(); 555 555 ··· 559 559 tries < 30 560 560 ) { 561 561 console.log( 562 - `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}`, 562 + `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}` 563 563 ); 564 564 existingTrack = await ctx.client.db.tracks 565 565 .filter( ··· 567 567 equals( 568 568 createHash("sha256") 569 569 .update( 570 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 570 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 571 571 ) 572 - .digest("hex"), 573 - ), 572 + .digest("hex") 573 + ) 574 574 ) 575 575 .getFirst(); 576 576 ··· 582 582 equals( 583 583 createHash("sha256") 584 584 .update(track.albumArtist.toLowerCase()) 585 - .digest("hex"), 586 - ), 585 + .digest("hex") 586 + ) 587 587 ) 588 588 .getFirst(); 589 589 if (artist) { ··· 602 602 equals( 603 603 createHash("sha256") 604 604 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 605 - .digest("hex"), 606 - ), 605 + .digest("hex") 606 + ) 607 607 ) 608 608 .getFirst(); 609 609 if (album) { ··· 630 630 631 631 if (existingTrack?.artist_uri) { 632 632 console.log( 633 - `Artist uri ready: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, 633 + `Artist uri ready: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 634 634 ); 635 635 } 636 636
+11 -11
apps/api/src/spotify/app.ts
··· 1 1 import { equals } from "@xata.io/client"; 2 2 import { ctx } from "context"; 3 - import crypto, { createHash } from "crypto"; 4 3 import { Hono } from "hono"; 5 4 import jwt from "jsonwebtoken"; 6 5 import { decrypt, encrypt } from "lib/crypto"; 7 6 import { env } from "lib/env"; 8 7 import { requestCounter } from "metrics"; 8 + import crypto, { createHash } from "node:crypto"; 9 9 import { rateLimiter } from "ratelimiter"; 10 10 import { emailSchema } from "types/email"; 11 11 ··· 17 17 limit: 10, // max Spotify API calls 18 18 window: 15, // per 10 seconds 19 19 keyPrefix: "spotify-ratelimit", 20 - }), 20 + }) 21 21 ); 22 22 23 23 app.get("/login", async (c) => { ··· 44 44 const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${env.SPOTIFY_CLIENT_ID}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=user-read-private%20user-read-email%20user-read-playback-state%20user-read-currently-playing%20user-modify-playback-state%20playlist-modify-public%20playlist-modify-private%20playlist-read-private%20playlist-read-collaborative&state=${state}`; 45 45 c.header( 46 46 "Set-Cookie", 47 - `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`, 47 + `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure` 48 48 ); 49 49 return c.json({ redirectUrl }); 50 50 }); ··· 210 210 211 211 const sha256 = createHash("sha256") 212 212 .update( 213 - `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase(), 213 + `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase() 214 214 ) 215 215 .digest("hex"); 216 216 ··· 264 264 265 265 const refreshToken = decrypt( 266 266 spotifyToken.refresh_token, 267 - env.SPOTIFY_ENCRYPTION_KEY, 267 + env.SPOTIFY_ENCRYPTION_KEY 268 268 ); 269 269 270 270 // get new access token ··· 330 330 331 331 const refreshToken = decrypt( 332 332 spotifyToken.refresh_token, 333 - env.SPOTIFY_ENCRYPTION_KEY, 333 + env.SPOTIFY_ENCRYPTION_KEY 334 334 ); 335 335 336 336 // get new access token ··· 396 396 397 397 const refreshToken = decrypt( 398 398 spotifyToken.refresh_token, 399 - env.SPOTIFY_ENCRYPTION_KEY, 399 + env.SPOTIFY_ENCRYPTION_KEY 400 400 ); 401 401 402 402 // get new access token ··· 462 462 463 463 const refreshToken = decrypt( 464 464 spotifyToken.refresh_token, 465 - env.SPOTIFY_ENCRYPTION_KEY, 465 + env.SPOTIFY_ENCRYPTION_KEY 466 466 ); 467 467 468 468 // get new access token ··· 488 488 headers: { 489 489 Authorization: `Bearer ${access_token}`, 490 490 }, 491 - }, 491 + } 492 492 ); 493 493 494 494 if (response.status === 403) { ··· 531 531 532 532 const refreshToken = decrypt( 533 533 spotifyToken.refresh_token, 534 - env.SPOTIFY_ENCRYPTION_KEY, 534 + env.SPOTIFY_ENCRYPTION_KEY 535 535 ); 536 536 537 537 // get new access token ··· 558 558 headers: { 559 559 Authorization: `Bearer ${access_token}`, 560 560 }, 561 - }, 561 + } 562 562 ); 563 563 564 564 if (response.status === 403) {
+13 -13
apps/api/src/tracks/tracks.service.ts
··· 1 1 import type { Agent } from "@atproto/api"; 2 2 import { equals } from "@xata.io/client"; 3 3 import type { Context } from "context"; 4 - import { createHash } from "crypto"; 4 + import { createHash } from "node:crypto"; 5 5 import { 6 6 putAlbumRecord, 7 7 putArtistRecord, ··· 16 16 equals( 17 17 createHash("sha256") 18 18 .update( 19 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 19 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 20 20 ) 21 - .digest("hex"), 22 - ), 21 + .digest("hex") 22 + ) 23 23 ) 24 24 .getFirst(); 25 25 ··· 36 36 equals( 37 37 createHash("sha256") 38 38 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 39 - .digest("hex"), 40 - ), 39 + .digest("hex") 40 + ) 41 41 ) 42 42 .getFirst(); 43 43 if (album) { ··· 54 54 equals( 55 55 createHash("sha256") 56 56 .update(track.albumArtist.toLowerCase()) 57 - .digest("hex"), 58 - ), 57 + .digest("hex") 58 + ) 59 59 ) 60 60 .getFirst(); 61 61 if (artist) { ··· 72 72 equals( 73 73 createHash("sha256") 74 74 .update(track.albumArtist.toLocaleLowerCase()) 75 - .digest("hex"), 76 - ), 75 + .digest("hex") 76 + ) 77 77 ) 78 78 .getFirst(); 79 79 ··· 88 88 equals( 89 89 createHash("sha256") 90 90 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 91 - .digest("hex"), 92 - ), 91 + .digest("hex") 92 + ) 93 93 ) 94 94 .getFirst(); 95 95 ··· 116 116 if (!track_id || !album_id || !artist_id) { 117 117 console.log( 118 118 "Track not yet saved (uri not saved), retrying...", 119 - tries + 1, 119 + tries + 1 120 120 ); 121 121 await new Promise((resolve) => setTimeout(resolve, 1000)); 122 122 tries += 1;
+15 -15
apps/api/src/websocket/handler.ts
··· 1 1 import chalk from "chalk"; 2 2 import { ctx } from "context"; 3 - import { createHash } from "crypto"; 4 3 import { and, eq } from "drizzle-orm"; 5 4 import type { Context } from "hono"; 6 5 import jwt from "jsonwebtoken"; 7 6 import { env } from "lib/env"; 7 + import { createHash } from "node:crypto"; 8 8 import lovedTracks from "schema/loved-tracks"; 9 9 import tracks from "schema/tracks"; 10 10 import users from "schema/users"; ··· 70 70 if (data.type === "track") { 71 71 const sha256 = createHash("sha256") 72 72 .update( 73 - `${data.title} - ${data.artist} - ${data.album}`.toLowerCase(), 73 + `${data.title} - ${data.artist} - ${data.album}`.toLowerCase() 74 74 ) 75 75 .digest("hex"); 76 76 const [cachedTrack, cachedLikes] = await Promise.all([ ··· 93 93 await ctx.redis.setEx( 94 94 `likes:${did}:${sha256}`, 95 95 2, 96 - JSON.stringify({ liked: data.liked }), 96 + JSON.stringify({ liked: data.liked }) 97 97 ); 98 98 } 99 99 ··· 113 113 ...data, 114 114 sha256, 115 115 liked: data.liked, 116 - }), 116 + }) 117 117 ); 118 118 } else { 119 119 const [track] = await ctx.db ··· 136 136 albumUri: track.albumUri, 137 137 artistUri: track.artistUri, 138 138 liked: data.liked, 139 - }), 139 + }) 140 140 ), 141 141 ctx.redis.setEx( 142 142 `nowplaying:${did}`, ··· 145 145 ...data, 146 146 sha256, 147 147 liked: data.liked, 148 - }), 148 + }) 149 149 ), 150 150 ]); 151 151 } ··· 154 154 await ctx.redis.setEx( 155 155 `nowplaying:${did}:status`, 156 156 3, 157 - `${data.status}`, 157 + `${data.status}` 158 158 ); 159 159 } 160 160 ··· 163 163 type: "message", 164 164 data, 165 165 device_id, 166 - }), 166 + }) 167 167 ); 168 168 } 169 169 }); ··· 175 175 ignoreExpiration: true, 176 176 }); 177 177 console.log( 178 - `Control message: ${chalk.greenBright(type)}, ${chalk.greenBright(target)}, ${chalk.greenBright(action)}, ${chalk.greenBright(args)}, ${chalk.greenBright("***")}`, 178 + `Control message: ${chalk.greenBright(type)}, ${chalk.greenBright(target)}, ${chalk.greenBright(action)}, ${chalk.greenBright(args)}, ${chalk.greenBright("***")}` 179 179 ); 180 180 // Handle control message 181 181 const deviceId = userDevices[did]?.find((id) => id === target); ··· 184 184 if (targetDevice) { 185 185 targetDevice.send(JSON.stringify({ type, action, args })); 186 186 console.log( 187 - `Control message sent to device: ${chalk.greenBright(deviceId)}, ${chalk.greenBright(target)}`, 187 + `Control message sent to device: ${chalk.greenBright(deviceId)}, ${chalk.greenBright(target)}` 188 188 ); 189 189 return; 190 190 } ··· 196 196 if (targetDevice) { 197 197 targetDevice.send(JSON.stringify({ type, action, args })); 198 198 console.log( 199 - `Control message sent to all devices: ${chalk.greenBright(id)}, ${chalk.greenBright(target)}`, 199 + `Control message sent to all devices: ${chalk.greenBright(id)}, ${chalk.greenBright(target)}` 200 200 ); 201 201 } 202 202 }); ··· 208 208 if (registerMessage.success) { 209 209 const { type, clientName, token } = registerMessage.data; 210 210 console.log( 211 - `Register message: ${chalk.greenBright(type)}, ${chalk.greenBright(clientName)}, ${chalk.greenBright("****")}`, 211 + `Register message: ${chalk.greenBright(type)}, ${chalk.greenBright(clientName)}, ${chalk.greenBright("****")}` 212 212 ); 213 213 // Handle register Message 214 214 const { did } = jwt.verify(token, env.JWT_SECRET, { ··· 221 221 deviceNames[deviceId] = clientName; 222 222 userDevices[did] = [...(userDevices[did] || []), deviceId]; 223 223 console.log( 224 - `Device registered: ${chalk.greenBright(deviceId)}, ${chalk.greenBright(clientName)}`, 224 + `Device registered: ${chalk.greenBright(deviceId)}, ${chalk.greenBright(clientName)}` 225 225 ); 226 226 227 227 // broadcast to all devices ··· 235 235 type: "device_registered", 236 236 deviceId, 237 237 clientName, 238 - }), 238 + }) 239 239 ); 240 240 } 241 241 }); ··· 266 266 const clientName = deviceNames[deviceId]; 267 267 delete deviceNames[deviceId]; 268 268 console.log( 269 - `Device name removed: ${chalk.redBright(deviceId)}, ${chalk.redBright(clientName)}`, 269 + `Device name removed: ${chalk.redBright(deviceId)}, ${chalk.redBright(clientName)}` 270 270 ); 271 271 } 272 272 },