A decentralized music tracking and discovery platform built on AT Protocol 🎵

Refactor code for consistency and readability

- Removed unnecessary spaces and added missing commas in various files to ensure consistent formatting.
- Updated logging and error handling to maintain uniformity across the codebase.
- Improved the structure of function calls and pipe operations for better clarity.
- Ensured all promises resolve correctly and consistently handle errors.

+404 -404
+1 -1
apps/api/src/lexicon/types/app/rocksky/apikey/createApikey.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyApikeyDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The name of the API key. */
+1 -1
apps/api/src/lexicon/types/app/rocksky/apikey/updateApikey.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyApikeyDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The ID of the API key to update. */
+1 -1
apps/api/src/lexicon/types/app/rocksky/like/dislikeShout.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyShoutDefs from "../shout/defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The unique identifier of the shout to dislike */
+1 -1
apps/api/src/lexicon/types/app/rocksky/like/dislikeSong.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskySongDefs from "../song/defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The unique identifier of the song to dislike */
+1 -1
apps/api/src/lexicon/types/app/rocksky/like/likeShout.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyShoutDefs from "../shout/defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The unique identifier of the shout to like */
+1 -1
apps/api/src/lexicon/types/app/rocksky/like/likeSong.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskySongDefs from "../song/defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The unique identifier of the song to like */
+1 -1
apps/api/src/lexicon/types/app/rocksky/scrobble/createScrobble.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyScrobbleDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The title of the track being scrobbled */
+1 -1
apps/api/src/lexicon/types/app/rocksky/shout/createShout.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyShoutDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The content of the shout */
+1 -1
apps/api/src/lexicon/types/app/rocksky/shout/replyShout.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyShoutDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The unique identifier of the shout to reply to */
+1 -1
apps/api/src/lexicon/types/app/rocksky/shout/reportShout.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskyShoutDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The unique identifier of the shout to report */
+1 -1
apps/api/src/lexicon/types/app/rocksky/song/createSong.ts
··· 9 9 import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 import type * as AppRockskySongDefs from "./defs"; 11 11 12 - export type QueryParams = {} 12 + export type QueryParams = {}; 13 13 14 14 export interface InputSchema { 15 15 /** The title of the song */
+1 -1
apps/api/src/lexicon/types/app/rocksky/spotify/next.ts
··· 8 8 import { CID } from "multiformats/cid"; 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 11 - export type QueryParams = {} 11 + export type QueryParams = {}; 12 12 13 13 export type InputSchema = undefined; 14 14 export type HandlerInput = undefined;
+1 -1
apps/api/src/lexicon/types/app/rocksky/spotify/pause.ts
··· 8 8 import { CID } from "multiformats/cid"; 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 11 - export type QueryParams = {} 11 + export type QueryParams = {}; 12 12 13 13 export type InputSchema = undefined; 14 14 export type HandlerInput = undefined;
+1 -1
apps/api/src/lexicon/types/app/rocksky/spotify/play.ts
··· 8 8 import { CID } from "multiformats/cid"; 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 11 - export type QueryParams = {} 11 + export type QueryParams = {}; 12 12 13 13 export type InputSchema = undefined; 14 14 export type HandlerInput = undefined;
+1 -1
apps/api/src/lexicon/types/app/rocksky/spotify/previous.ts
··· 8 8 import { CID } from "multiformats/cid"; 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 10 11 - export type QueryParams = {} 11 + export type QueryParams = {}; 12 12 13 13 export type InputSchema = undefined; 14 14 export type HandlerInput = undefined;
+2 -2
apps/api/src/lib/crypto.ts
··· 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");
+17 -17
apps/api/src/lovedtracks/lovedtracks.service.ts
··· 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.*"])
+32 -32
apps/api/src/nowplaying/nowplaying.service.ts
··· 13 13 14 14 export async function putArtistRecord( 15 15 track: Track, 16 - agent: Agent 16 + agent: Agent, 17 17 ): Promise<string | null> { 18 18 const rkey = TID.nextStr(); 19 19 const record: { ··· 54 54 55 55 export async function putAlbumRecord( 56 56 track: Track, 57 - agent: Agent 57 + agent: Agent, 58 58 ): Promise<string | null> { 59 59 const rkey = TID.nextStr(); 60 60 ··· 94 94 95 95 export async function putSongRecord( 96 96 track: Track, 97 - agent: Agent 97 + agent: Agent, 98 98 ): Promise<string | null> { 99 99 const rkey = TID.nextStr(); 100 100 ··· 146 146 147 147 async function putScrobbleRecord( 148 148 track: Track, 149 - agent: Agent 149 + agent: Agent, 150 150 ): Promise<string | null> { 151 151 const rkey = TID.nextStr(); 152 152 ··· 308 308 ctx: Context, 309 309 track: Track, 310 310 agent: Agent, 311 - userDid: string 311 + userDid: string, 312 312 ): Promise<void> { 313 313 // check if scrobble already exists (user did + timestamp) 314 314 const scrobbleTime = dayjs.unix(track.timestamp); ··· 332 332 if (existingScrobble) { 333 333 console.log( 334 334 `Scrobble already exists for ${chalk.cyan(track.title)} at ${chalk.cyan( 335 - dayjs.unix(track.timestamp).format("YYYY-MM-DD HH:mm:ss") 336 - )}` 335 + dayjs.unix(track.timestamp).format("YYYY-MM-DD HH:mm:ss"), 336 + )}`, 337 337 ); 338 338 return; 339 339 } ··· 345 345 equals( 346 346 createHash("sha256") 347 347 .update( 348 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 348 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 349 349 ) 350 - .digest("hex") 351 - ) 350 + .digest("hex"), 351 + ), 352 352 ) 353 353 .getFirst(); 354 354 ··· 359 359 equals( 360 360 createHash("sha256") 361 361 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 362 - .digest("hex") 363 - ) 362 + .digest("hex"), 363 + ), 364 364 ) 365 365 .getFirst(); 366 366 if (album) { ··· 377 377 equals( 378 378 createHash("sha256") 379 379 .update(track.albumArtist.toLowerCase()) 380 - .digest("hex") 381 - ) 380 + .digest("hex"), 381 + ), 382 382 ) 383 383 .getFirst(); 384 384 if (artist) { ··· 405 405 equals( 406 406 createHash("sha256") 407 407 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 408 - .digest("hex") 409 - ) 408 + .digest("hex"), 409 + ), 410 410 ) 411 411 .getFirst(); 412 412 ··· 419 419 equals( 420 420 createHash("sha256") 421 421 .update( 422 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 422 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 423 423 ) 424 - .digest("hex") 425 - ) 424 + .digest("hex"), 425 + ), 426 426 ) 427 427 .getFirst(); 428 428 await new Promise((resolve) => setTimeout(resolve, 1000)); ··· 435 435 436 436 if (existingTrack) { 437 437 console.log( 438 - `Song found: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 438 + `Song found: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, 439 439 ); 440 440 } 441 441 ··· 485 485 equals( 486 486 createHash("sha256") 487 487 .update( 488 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 488 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 489 489 ) 490 - .digest("hex") 491 - ) 490 + .digest("hex"), 491 + ), 492 492 ) 493 493 .getFirst(); 494 494 ··· 498 498 tries < 30 499 499 ) { 500 500 console.log( 501 - `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}` 501 + `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}`, 502 502 ); 503 503 existingTrack = await ctx.client.db.tracks 504 504 .filter( ··· 506 506 equals( 507 507 createHash("sha256") 508 508 .update( 509 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 509 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 510 510 ) 511 - .digest("hex") 512 - ) 511 + .digest("hex"), 512 + ), 513 513 ) 514 514 .getFirst(); 515 515 ··· 521 521 equals( 522 522 createHash("sha256") 523 523 .update(track.albumArtist.toLowerCase()) 524 - .digest("hex") 525 - ) 524 + .digest("hex"), 525 + ), 526 526 ) 527 527 .getFirst(); 528 528 if (artist) { ··· 541 541 equals( 542 542 createHash("sha256") 543 543 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 544 - .digest("hex") 545 - ) 544 + .digest("hex"), 545 + ), 546 546 ) 547 547 .getFirst(); 548 548 if (album) { ··· 569 569 570 570 if (existingTrack?.artist_uri) { 571 571 console.log( 572 - `Artist uri ready: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 572 + `Artist uri ready: ${chalk.cyan(existingTrack.xata_id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, 573 573 ); 574 574 } 575 575
+10 -10
apps/api/src/spotify/app.ts
··· 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) {
+4 -4
apps/api/src/subscribers/playlist.ts
··· 17 17 did: string; 18 18 } = JSON.parse(sc.decode(m.data)); 19 19 console.log( 20 - `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}` 20 + `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}`, 21 21 ); 22 22 await putPlaylistRecord(ctx, payload); 23 23 } ··· 26 26 27 27 async function putPlaylistRecord( 28 28 ctx: Context, 29 - payload: { id: string; did: string } 29 + payload: { id: string; did: string }, 30 30 ) { 31 31 const agent = await createAgent(ctx.oauthClient, payload.did); 32 32 33 33 if (!agent) { 34 34 console.error( 35 - `Failed to create agent, skipping playlist: ${chalk.cyan(payload.id)} for ${chalk.greenBright(payload.did)}` 35 + `Failed to create agent, skipping playlist: ${chalk.cyan(payload.id)} for ${chalk.greenBright(payload.did)}`, 36 36 ); 37 37 return; 38 38 } ··· 100 100 101 101 await ctx.meilisearch.post( 102 102 `indexes/playlists/documents?primaryKey=id`, 103 - updatedPlaylist 103 + updatedPlaylist, 104 104 ); 105 105 }
+12 -12
apps/api/src/tracks/tracks.service.ts
··· 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;
+14 -14
apps/api/src/websocket/handler.ts
··· 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 },
+1 -1
apps/api/src/xata.ts
··· 5255 5255 } 5256 5256 } 5257 5257 5258 - let instance: XataClient | undefined ; 5258 + let instance: XataClient | undefined; 5259 5259 5260 5260 export const getXataClient = () => { 5261 5261 if (instance) return instance;
+173 -173
apps/api/src/xrpc/app/rocksky/scrobble/createScrobble.ts
··· 37 37 pipe( 38 38 scrobbleTrack(ctx, track, agent, did), 39 39 Effect.tap(() => 40 - Effect.logInfo(`Scrobble created for ${chalk.cyan(track.title)}`) 41 - ) 42 - ) 40 + Effect.logInfo(`Scrobble created for ${chalk.cyan(track.title)}`), 41 + ), 42 + ), 43 43 ), 44 44 Effect.flatMap(presentation), 45 45 Effect.retry({ times: 3 }), ··· 47 47 Effect.catchAll((err) => { 48 48 console.error(err); 49 49 return Effect.succeed({}); 50 - }) 50 + }), 51 51 ); 52 52 server.app.rocksky.scrobble.createScrobble({ 53 53 auth: ctx.authVerifier, ··· 81 81 ctx, 82 82 did, 83 83 input, 84 - })) 84 + })), 85 85 ), 86 86 Match.orElse(() => { 87 87 throw new Error("Authentication required to create a scrobble"); 88 - }) 88 + }), 89 89 ), 90 90 catch: (error) => new Error(`Failed to create agent: ${error}`), 91 91 }); ··· 124 124 agent: Agent, 125 125 collection: string, 126 126 record: T, 127 - validate: (record: T) => { success: boolean } 127 + validate: (record: T) => { success: boolean }, 128 128 ) => 129 129 pipe( 130 130 Effect.succeed(record), 131 131 Effect.filterOrFail( 132 132 (rec) => validate(rec).success, 133 - () => new Error("Invalid record") 133 + () => new Error("Invalid record"), 134 134 ), 135 135 Effect.flatMap(() => 136 136 pipe( ··· 143 143 rkey, 144 144 record, 145 145 validate: false, 146 - }) 147 - ) 146 + }), 147 + ), 148 148 ), 149 149 Effect.tap((res) => 150 - Effect.logInfo(`Record created at ${res.data.uri}`) 150 + Effect.logInfo(`Record created at ${res.data.uri}`), 151 151 ), 152 - Effect.map((res) => res.data.uri) 153 - ) 152 + Effect.map((res) => res.data.uri), 153 + ), 154 154 ), 155 155 Effect.catchAll((error) => { 156 156 console.error(`Error creating ${collection} record`, error); 157 157 return Effect.succeed(null); 158 - }) 158 + }), 159 159 ); 160 160 161 161 const putArtistRecord = (track: Track, agent: Agent) => ··· 168 168 tags: track.genres, 169 169 }), 170 170 Effect.flatMap((record) => 171 - putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord) 172 - ) 171 + putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord), 172 + ), 173 173 ); 174 174 175 175 const putAlbumRecord = (track: Track, agent: Agent) => ··· 186 186 albumArtUrl: track.albumArt, 187 187 }), 188 188 Effect.flatMap((record) => 189 - putRecord(agent, "app.rocksky.album", record, Album.validateRecord) 190 - ) 189 + putRecord(agent, "app.rocksky.album", record, Album.validateRecord), 190 + ), 191 191 ); 192 192 193 193 const putSongRecord = (track: Track, agent: Agent) => ··· 213 213 spotifyLink: track.spotifyLink ?? undefined, 214 214 }), 215 215 Effect.flatMap((record) => 216 - putRecord(agent, "app.rocksky.song", record, Song.validateRecord) 217 - ) 216 + putRecord(agent, "app.rocksky.song", record, Song.validateRecord), 217 + ), 218 218 ); 219 219 220 220 const putScrobbleRecord = (track: Track, agent: Agent) => ··· 242 242 spotifyLink: track.spotifyLink ?? undefined, 243 243 }), 244 244 Effect.flatMap((record) => 245 - putRecord(agent, "app.rocksky.scrobble", record, Scrobble.validateRecord) 246 - ) 245 + putRecord(agent, "app.rocksky.scrobble", record, Scrobble.validateRecord), 246 + ), 247 247 ); 248 248 249 249 const getScrobble = ({ ctx, id }: { ctx: Context; id: string }) => ··· 255 255 .leftJoin(tables.albums, eq(tables.albums.id, tables.scrobbles.albumId)) 256 256 .leftJoin( 257 257 tables.artists, 258 - eq(tables.artists.id, tables.scrobbles.artistId) 258 + eq(tables.artists.id, tables.scrobbles.artistId), 259 259 ) 260 260 .leftJoin(tables.users, eq(tables.users.id, tables.scrobbles.userId)) 261 261 .where(eq(tables.scrobbles.id, id)) 262 262 .execute() 263 - .then(([row]) => row) 263 + .then(([row]) => row), 264 264 ); 265 265 266 266 const getUserAlbum = ( ··· 270 270 artists: SelectArtist; 271 271 users: SelectUser; 272 272 tracks: SelectTrack; 273 - } 273 + }, 274 274 ) => 275 275 Effect.tryPromise(() => 276 276 ctx.db ··· 278 278 .from(tables.userAlbums) 279 279 .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 280 280 .execute() 281 - .then(([row]) => row) 281 + .then(([row]) => row), 282 282 ); 283 283 284 284 const getUserArtist = ( ··· 288 288 artists: SelectArtist; 289 289 users: SelectUser; 290 290 tracks: SelectTrack; 291 - } 291 + }, 292 292 ) => 293 293 Effect.tryPromise(() => 294 294 ctx.db ··· 296 296 .from(tables.userArtists) 297 297 .where(eq(tables.userArtists.id, scrobble.artists.id)) 298 298 .execute() 299 - .then(([row]) => row) 299 + .then(([row]) => row), 300 300 ); 301 301 302 302 const getUserTrack = ( ··· 306 306 artists: SelectArtist; 307 307 users: SelectUser; 308 308 tracks: SelectTrack; 309 - } 309 + }, 310 310 ) => 311 311 Effect.tryPromise(() => 312 312 ctx.db ··· 314 314 .from(tables.userTracks) 315 315 .where(eq(tables.userTracks.id, scrobble.tracks.id)) 316 316 .execute() 317 - .then(([row]) => row) 317 + .then(([row]) => row), 318 318 ); 319 319 320 320 const getAlbumTrack = ( ··· 324 324 artists: SelectArtist; 325 325 users: SelectUser; 326 326 tracks: SelectTrack; 327 - } 327 + }, 328 328 ) => 329 329 Effect.tryPromise(() => 330 330 ctx.db ··· 332 332 .from(tables.albumTracks) 333 333 .where(eq(tables.albumTracks.trackId, scrobble.tracks.id)) 334 334 .execute() 335 - .then(([row]) => row) 335 + .then(([row]) => row), 336 336 ); 337 337 338 338 const getArtistTrack = ( ··· 342 342 artists: SelectArtist; 343 343 users: SelectUser; 344 344 tracks: SelectTrack; 345 - } 345 + }, 346 346 ) => 347 347 Effect.tryPromise(() => 348 348 ctx.db ··· 350 350 .from(tables.artistTracks) 351 351 .where(eq(tables.artistTracks.trackId, scrobble.tracks.id)) 352 352 .execute() 353 - .then(([row]) => row) 353 + .then(([row]) => row), 354 354 ); 355 355 356 356 const getArtistAlbum = ( ··· 360 360 artists: SelectArtist; 361 361 users: SelectUser; 362 362 tracks: SelectTrack; 363 - } 363 + }, 364 364 ) => 365 365 Effect.tryPromise(() => 366 366 ctx.db ··· 369 369 .where( 370 370 and( 371 371 eq(tables.artistAlbums.albumId, scrobble.albums.id), 372 - eq(tables.artistAlbums.artistId, scrobble.artists.id) 373 - ) 372 + eq(tables.artistAlbums.artistId, scrobble.artists.id), 373 + ), 374 374 ) 375 - .then(([row]) => row) 375 + .then(([row]) => row), 376 376 ); 377 377 378 378 const createUserArtist = ( ··· 382 382 artists: SelectArtist; 383 383 users: SelectUser; 384 384 tracks: SelectTrack; 385 - } 385 + }, 386 386 ) => 387 387 pipe( 388 388 Effect.tryPromise(() => ··· 394 394 uri: scrobble.artists.uri, 395 395 scrobbles: 1, 396 396 } as InsertUserArtist) 397 - .execute() 397 + .execute(), 398 398 ), 399 399 Effect.flatMap(() => 400 400 Effect.tryPromise(() => ··· 403 403 .from(tables.userArtists) 404 404 .where(eq(tables.userArtists.artistId, scrobble.artists.id)) 405 405 .execute() 406 - .then(([row]) => row) 407 - ) 408 - ) 406 + .then(([row]) => row), 407 + ), 408 + ), 409 409 ); 410 410 411 411 const createUserAlbum = ( ··· 415 415 artists: SelectArtist; 416 416 users: SelectUser; 417 417 tracks: SelectTrack; 418 - } 418 + }, 419 419 ) => 420 420 pipe( 421 421 Effect.tryPromise(() => ··· 427 427 uri: scrobble.albums.uri, 428 428 scrobbles: 1, 429 429 } as InsertUserAlbum) 430 - .execute() 430 + .execute(), 431 431 ), 432 432 Effect.flatMap(() => 433 433 Effect.tryPromise(() => ··· 436 436 .from(tables.userAlbums) 437 437 .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 438 438 .execute() 439 - .then(([row]) => row) 440 - ) 441 - ) 439 + .then(([row]) => row), 440 + ), 441 + ), 442 442 ); 443 443 444 444 const createUserTrack = ( ··· 448 448 artists: SelectArtist; 449 449 users: SelectUser; 450 450 tracks: SelectTrack; 451 - } 451 + }, 452 452 ) => 453 453 pipe( 454 454 Effect.tryPromise(() => ··· 460 460 uri: scrobble.tracks.uri, 461 461 scrobbles: 1, 462 462 } as InsertUserTrack) 463 - .execute() 463 + .execute(), 464 464 ), 465 465 Effect.flatMap(() => 466 466 Effect.tryPromise(() => ··· 468 468 .select() 469 469 .from(tables.userTracks) 470 470 .where(eq(tables.userTracks.trackId, scrobble.tracks.id)) 471 - .then(([row]) => row) 472 - ) 473 - ) 471 + .then(([row]) => row), 472 + ), 473 + ), 474 474 ); 475 475 476 476 const publishScrobble = (ctx: Context, id: string) => ··· 641 641 xata_updatedat: artistAlbum.updatedAt.toISOString(), 642 642 xata_version: artistAlbum.xataVersion, 643 643 }, 644 - }) 645 - ) 646 - ) 647 - ) 648 - ) 644 + }), 645 + ), 646 + ), 647 + ), 648 + ), 649 649 ), 650 650 Effect.flatMap((data) => 651 651 Effect.try(() => 652 652 ctx.nc.publish( 653 653 "rocksky.scrobble", 654 654 Buffer.from( 655 - JSON.stringify(data).replaceAll("sha_256", "sha256") 656 - ) 657 - ) 658 - ) 659 - ) 660 - ) 661 - ) 662 - ) 663 - ) 655 + JSON.stringify(data).replaceAll("sha_256", "sha256"), 656 + ), 657 + ), 658 + ), 659 + ), 660 + ), 661 + ), 662 + ), 663 + ), 664 664 ); 665 665 666 666 const computeTrackHash = (track: Track): Effect.Effect<string, never> => 667 667 Effect.succeed( 668 668 createHash("sha256") 669 669 .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) 670 - .digest("hex") 670 + .digest("hex"), 671 671 ); 672 672 673 673 const computeAlbumHash = (track: Track): Effect.Effect<string, never> => 674 674 Effect.succeed( 675 675 createHash("sha256") 676 676 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 677 - .digest("hex") 677 + .digest("hex"), 678 678 ); 679 679 680 680 const computeArtistHash = (track: Track): Effect.Effect<string, never> => 681 681 Effect.succeed( 682 - createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex") 682 + createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex"), 683 683 ); 684 684 685 685 const fetchExistingTrack = ( 686 686 ctx: Context, 687 - trackHash: string 687 + trackHash: string, 688 688 ): Effect.Effect<SelectTrack | undefined, Error> => 689 689 Effect.tryPromise(() => 690 690 ctx.db ··· 692 692 .from(tables.tracks) 693 693 .where(eq(tables.tracks.sha256, trackHash)) 694 694 .execute() 695 - .then(([row]) => row) 695 + .then(([row]) => row), 696 696 ); 697 697 698 698 // Update track metadata (album_uri and artist_uri) 699 699 const updateTrackMetadata = ( 700 700 ctx: Context, 701 701 track: Track, 702 - trackRecord: SelectTrack 702 + trackRecord: SelectTrack, 703 703 ) => 704 704 pipe( 705 705 Effect.succeed(trackRecord), ··· 714 714 .from(tables.albums) 715 715 .where(eq(tables.albums.sha256, albumHash)) 716 716 .execute() 717 - .then(([row]) => row) 718 - ) 717 + .then(([row]) => row), 718 + ), 719 719 ), 720 720 Effect.flatMap((album) => 721 721 album ··· 726 726 albumUri: album.uri, 727 727 }) 728 728 .where(eq(tables.tracks.id, trackRecord.id)) 729 - .execute() 729 + .execute(), 730 730 ) 731 - : Effect.succeed(undefined) 732 - ) 731 + : Effect.succeed(undefined), 732 + ), 733 733 ) 734 - : Effect.succeed(undefined) 734 + : Effect.succeed(undefined), 735 735 ), 736 736 Effect.tap((trackRecord) => 737 737 !trackRecord.artistUri ··· 744 744 .from(tables.artists) 745 745 .where(eq(tables.artists.sha256, artistHash)) 746 746 .execute() 747 - .then(([row]) => row) 748 - ) 747 + .then(([row]) => row), 748 + ), 749 749 ), 750 750 Effect.flatMap((artist) => 751 751 artist ··· 756 756 artistUri: artist.uri, 757 757 }) 758 758 .where(eq(tables.tracks.id, trackRecord.id)) 759 - .execute() 759 + .execute(), 760 760 ) 761 - : Effect.succeed(undefined) 762 - ) 761 + : Effect.succeed(undefined), 762 + ), 763 763 ) 764 - : Effect.succeed(undefined) 765 - ) 764 + : Effect.succeed(undefined), 765 + ), 766 766 ); 767 767 768 768 // Ensure track exists or create it ··· 771 771 track: Track, 772 772 agent: Agent, 773 773 userDid: string, 774 - existingTrack: SelectTrack | undefined 774 + existingTrack: SelectTrack | undefined, 775 775 ) => 776 776 pipe( 777 777 Effect.succeed(existingTrack), ··· 779 779 Match.value(trackOpt).pipe( 780 780 Match.when( 781 781 (value) => !!value, 782 - () => updateTrackMetadata(ctx, track, trackOpt) 782 + () => updateTrackMetadata(ctx, track, trackOpt), 783 783 ), 784 - Match.orElse(() => Effect.succeed(undefined)) 785 - ) 784 + Match.orElse(() => Effect.succeed(undefined)), 785 + ), 786 786 ), 787 787 Effect.flatMap((trackOpt) => 788 788 pipe( ··· 792 792 .from(tables.userTracks) 793 793 .leftJoin( 794 794 tables.tracks, 795 - eq(tables.userTracks.trackId, tables.tracks.id) 795 + eq(tables.userTracks.trackId, tables.tracks.id), 796 796 ) 797 797 .leftJoin( 798 798 tables.users, 799 - eq(tables.userTracks.userId, tables.users.id) 799 + eq(tables.userTracks.userId, tables.users.id), 800 800 ) 801 801 .where( 802 802 and( 803 803 eq(tables.tracks.id, trackOpt?.id), 804 - eq(tables.users.did, userDid) 805 - ) 804 + eq(tables.users.did, userDid), 805 + ), 806 806 ) 807 807 .execute() 808 - .then(([row]) => row.user_tracks) 808 + .then(([row]) => row.user_tracks), 809 809 ), 810 810 Effect.flatMap((userTrack) => 811 811 Option.isNone(Option.fromNullable(userTrack)) || 812 812 !userTrack?.uri?.includes(userDid) 813 813 ? putSongRecord(track, agent) 814 - : Effect.succeed(null) 815 - ) 816 - ) 817 - ) 814 + : Effect.succeed(null), 815 + ), 816 + ), 817 + ), 818 818 ); 819 819 820 820 // Ensure album exists or create it ··· 822 822 ctx: Context, 823 823 track: Track, 824 824 agent: Agent, 825 - userDid: string 825 + userDid: string, 826 826 ) => 827 827 pipe( 828 828 computeAlbumHash(track), ··· 833 833 .from(tables.albums) 834 834 .where(eq(tables.albums.sha256, albumHash)) 835 835 .execute() 836 - .then(([row]) => row) 837 - ) 836 + .then(([row]) => row), 837 + ), 838 838 ), 839 839 Effect.flatMap((existingAlbum) => 840 840 pipe( ··· 846 846 .from(tables.userAlbums) 847 847 .leftJoin( 848 848 tables.albums, 849 - eq(tables.userAlbums.albumId, tables.albums.id) 849 + eq(tables.userAlbums.albumId, tables.albums.id), 850 850 ) 851 851 .leftJoin( 852 852 tables.users, 853 - eq(tables.userAlbums.userId, tables.users.id) 853 + eq(tables.userAlbums.userId, tables.users.id), 854 854 ) 855 855 .where( 856 856 and( 857 857 eq(tables.albums.id, album.id), 858 - eq(tables.users.did, userDid) 859 - ) 858 + eq(tables.users.did, userDid), 859 + ), 860 860 ) 861 861 .execute() 862 - .then(([row]) => row.user_albums) 863 - ) 862 + .then(([row]) => row.user_albums), 863 + ), 864 864 ), 865 865 Effect.flatMap((userAlbum) => 866 866 Option.isNone(Option.fromNullable(existingAlbum)) || 867 867 Option.isNone(Option.fromNullable(userAlbum)) || 868 868 !userAlbum?.uri?.includes(userDid) 869 869 ? putAlbumRecord(track, agent) 870 - : Effect.succeed(null) 871 - ) 872 - ) 873 - ) 870 + : Effect.succeed(null), 871 + ), 872 + ), 873 + ), 874 874 ); 875 875 876 876 // Ensure artist exists or create it ··· 878 878 ctx: Context, 879 879 track: Track, 880 880 agent: Agent, 881 - userDid: string 881 + userDid: string, 882 882 ) => 883 883 pipe( 884 884 computeArtistHash(track), ··· 889 889 .from(tables.artists) 890 890 .where(eq(tables.artists.sha256, artistHash)) 891 891 .execute() 892 - .then(([row]) => row) 893 - ) 892 + .then(([row]) => row), 893 + ), 894 894 ), 895 895 Effect.flatMap((existingArtist) => 896 896 pipe( ··· 902 902 .from(tables.userArtists) 903 903 .leftJoin( 904 904 tables.artists, 905 - eq(tables.userArtists.artistId, tables.artists.id) 905 + eq(tables.userArtists.artistId, tables.artists.id), 906 906 ) 907 907 .leftJoin( 908 908 tables.users, 909 - eq(tables.userArtists.userId, tables.users.id) 909 + eq(tables.userArtists.userId, tables.users.id), 910 910 ) 911 911 .where( 912 912 and( 913 913 eq(tables.artists.id, artist.id), 914 - eq(tables.users.did, userDid) 915 - ) 914 + eq(tables.users.did, userDid), 915 + ), 916 916 ) 917 917 .execute() 918 - .then(([row]) => row.user_artists) 919 - ) 918 + .then(([row]) => row.user_artists), 919 + ), 920 920 ), 921 921 Effect.flatMap((userArtist) => 922 922 Effect.if( ··· 926 926 { 927 927 onTrue: () => putArtistRecord(track, agent), 928 928 onFalse: () => Effect.succeed(null), 929 - } 930 - ) 931 - ) 932 - ) 933 - ) 929 + }, 930 + ), 931 + ), 932 + ), 933 + ), 934 934 ); 935 935 936 936 // Retry fetching track until metadata is ready 937 937 const retryFetchTrack = ( 938 938 ctx: Context, 939 939 trackHash: string, 940 - initialTrack: SelectTrack | undefined 940 + initialTrack: SelectTrack | undefined, 941 941 ) => 942 942 pipe( 943 943 Effect.iterate( ··· 953 953 .from(tables.tracks) 954 954 .where(eq(tables.tracks.sha256, trackHash)) 955 955 .execute() 956 - .then(([row]) => row) 956 + .then(([row]) => row), 957 957 ), 958 958 Effect.flatMap((trackRecord) => 959 959 Option.fromNullable(trackRecord).pipe( 960 960 Effect.flatMap((track) => 961 - updateTrackMetadata(ctx, track, trackRecord) 962 - ) 963 - ) 961 + updateTrackMetadata(ctx, track, trackRecord), 962 + ), 963 + ), 964 964 ), 965 965 Effect.tap((trackRecord) => 966 966 Effect.logInfo( 967 967 trackRecord 968 968 ? `Track metadata ready: ${chalk.cyan(trackRecord.id)} - ${track.title}, after ${chalk.magenta(tries + 1)} tries` 969 - : `Retrying track fetch: ${chalk.magenta(tries + 1)}` 970 - ) 969 + : `Retrying track fetch: ${chalk.magenta(tries + 1)}`, 970 + ), 971 971 ), 972 972 Effect.map((trackRecord) => ({ 973 973 tries: tries + 1, 974 974 track: trackRecord, 975 975 })), 976 - Effect.delay("1 second") 976 + Effect.delay("1 second"), 977 977 ), 978 - } 978 + }, 979 979 ), 980 980 Effect.tap(({ tries, track }) => 981 981 tries >= 30 && !(track?.artistUri && track?.albumUri) 982 982 ? Effect.logError( 983 - `Track metadata not ready after ${chalk.magenta("30 tries")}` 983 + `Track metadata not ready after ${chalk.magenta("30 tries")}`, 984 984 ) 985 - : Effect.succeed(undefined) 985 + : Effect.succeed(undefined), 986 986 ), 987 - Effect.map(({ track }) => track) 987 + Effect.map(({ track }) => track), 988 988 ); 989 989 990 990 // Retry fetching scrobble until complete ··· 1022 1022 .from(tables.scrobbles) 1023 1023 .leftJoin( 1024 1024 tables.tracks, 1025 - eq(tables.scrobbles.trackId, tables.tracks.id) 1025 + eq(tables.scrobbles.trackId, tables.tracks.id), 1026 1026 ) 1027 1027 .leftJoin( 1028 1028 tables.albums, 1029 - eq(tables.scrobbles.albumId, tables.albums.id) 1029 + eq(tables.scrobbles.albumId, tables.albums.id), 1030 1030 ) 1031 1031 .leftJoin( 1032 1032 tables.artists, 1033 - eq(tables.scrobbles.artistId, tables.artists.id) 1033 + eq(tables.scrobbles.artistId, tables.artists.id), 1034 1034 ) 1035 1035 .leftJoin( 1036 1036 tables.users, 1037 - eq(tables.scrobbles.userId, tables.users.id) 1037 + eq(tables.scrobbles.userId, tables.users.id), 1038 1038 ) 1039 1039 .where(eq(tables.scrobbles.uri, scrobbleUri)) 1040 1040 .execute() 1041 - .then(([row]) => row) 1041 + .then(([row]) => row), 1042 1042 ), 1043 1043 Effect.tap((scrobble) => 1044 1044 Effect.if( ··· 1055 1055 artistUri: scrobble.artists.uri, 1056 1056 }) 1057 1057 .where(eq(tables.albums.id, scrobble.albums.id)) 1058 - .execute() 1058 + .execute(), 1059 1059 ), 1060 1060 onFalse: () => Effect.succeed(undefined), 1061 - } 1062 - ) 1061 + }, 1062 + ), 1063 1063 ), 1064 1064 Effect.flatMap(() => 1065 1065 Effect.tryPromise(() => ··· 1068 1068 .from(tables.scrobbles) 1069 1069 .leftJoin( 1070 1070 tables.tracks, 1071 - eq(tables.scrobbles.trackId, tables.tracks.id) 1071 + eq(tables.scrobbles.trackId, tables.tracks.id), 1072 1072 ) 1073 1073 .leftJoin( 1074 1074 tables.albums, 1075 - eq(tables.scrobbles.albumId, tables.albums.id) 1075 + eq(tables.scrobbles.albumId, tables.albums.id), 1076 1076 ) 1077 1077 .leftJoin( 1078 1078 tables.artists, 1079 - eq(tables.scrobbles.artistId, tables.artists.id) 1079 + eq(tables.scrobbles.artistId, tables.artists.id), 1080 1080 ) 1081 1081 .leftJoin( 1082 1082 tables.users, 1083 - eq(tables.scrobbles.userId, tables.users.id) 1083 + eq(tables.scrobbles.userId, tables.users.id), 1084 1084 ) 1085 1085 .where(eq(tables.scrobbles.uri, scrobbleUri)) 1086 1086 .execute() 1087 - .then(([row]) => row) 1088 - ) 1087 + .then(([row]) => row), 1088 + ), 1089 1089 ), 1090 1090 Effect.map((scrobble) => ({ 1091 1091 tries: tries + 1, ··· 1102 1102 scrobble.tracks.albumUri && 1103 1103 scrobble.scrobbles 1104 1104 ? `Scrobble found after ${chalk.magenta(tries + 1)} tries` 1105 - : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}` 1106 - ) 1105 + : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}`, 1106 + ), 1107 1107 ), 1108 - Effect.delay("1 second") 1108 + Effect.delay("1 second"), 1109 1109 ), 1110 - } 1110 + }, 1111 1111 ), 1112 1112 Effect.tap(({ tries, scrobble }) => 1113 1113 tries >= 30 && ··· 1121 1121 scrobble.tracks.albumUri 1122 1122 ) 1123 1123 ? Effect.logError( 1124 - `Scrobble not found after ${chalk.magenta("30 tries")}` 1124 + `Scrobble not found after ${chalk.magenta("30 tries")}`, 1125 1125 ) 1126 - : Effect.succeed(undefined) 1126 + : Effect.succeed(undefined), 1127 1127 ), 1128 - Effect.map(({ scrobble }) => scrobble) 1128 + Effect.map(({ scrobble }) => scrobble), 1129 1129 ); 1130 1130 1131 1131 export const scrobbleTrack = ( 1132 1132 ctx: Context, 1133 1133 track: Track, 1134 1134 agent: Agent, 1135 - userDid: string 1135 + userDid: string, 1136 1136 ) => 1137 1137 pipe( 1138 1138 computeTrackHash(track), ··· 1145 1145 Effect.flatMap(() => ensureAlbum(ctx, track, agent, userDid)), 1146 1146 Effect.flatMap(() => ensureArtist(ctx, track, agent, userDid)), 1147 1147 Effect.flatMap(() => 1148 - retryFetchTrack(ctx, trackHash, existingTrack) 1148 + retryFetchTrack(ctx, trackHash, existingTrack), 1149 1149 ), 1150 1150 Effect.flatMap(() => 1151 1151 pipe( ··· 1165 1165 ? pipe( 1166 1166 publishScrobble(ctx, scrobble.scrobbles.id), 1167 1167 Effect.tap(() => 1168 - Effect.logInfo("Scrobble published") 1169 - ) 1168 + Effect.logInfo("Scrobble published"), 1169 + ), 1170 1170 ) 1171 - : Effect.succeed(undefined) 1172 - ) 1173 - ) 1174 - ) 1175 - ) 1176 - ) 1177 - ) 1178 - ) 1179 - ) 1180 - ) 1171 + : Effect.succeed(undefined), 1172 + ), 1173 + ), 1174 + ), 1175 + ), 1176 + ), 1177 + ), 1178 + ), 1179 + ), 1180 + ), 1181 1181 );
+124 -124
apps/api/src/xrpc/app/rocksky/song/createSong.ts
··· 43 43 Effect.catchAll((err) => { 44 44 console.error(err); 45 45 return Effect.succeed({}); 46 - }) 46 + }), 47 47 ); 48 48 server.app.rocksky.song.createSong({ 49 49 auth: ctx.authVerifier, ··· 77 77 ctx, 78 78 did, 79 79 input, 80 - })) 80 + })), 81 81 ), 82 82 Match.orElse(() => { 83 83 throw new Error("Authentication required to create a song"); 84 - }) 84 + }), 85 85 ), 86 86 catch: (error) => new Error(`Failed to create agent: ${error}`), 87 87 }); ··· 150 150 Effect.succeed( 151 151 createHash("sha256") 152 152 .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) 153 - .digest("hex") 153 + .digest("hex"), 154 154 ); 155 155 156 156 const computeAlbumHash = (track: Track): Effect.Effect<string, never> => 157 157 Effect.succeed( 158 158 createHash("sha256") 159 159 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 160 - .digest("hex") 160 + .digest("hex"), 161 161 ); 162 162 163 163 const computeArtistHash = (track: Track): Effect.Effect<string, never> => 164 164 Effect.succeed( 165 - createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex") 165 + createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex"), 166 166 ); 167 167 168 168 const fetchExistingTrack = ( 169 169 ctx: Context, 170 - trackHash: string 170 + trackHash: string, 171 171 ): Effect.Effect<SelectTrack | undefined, Error> => 172 172 Effect.tryPromise(() => 173 173 ctx.db ··· 175 175 .from(tables.tracks) 176 176 .where(eq(tables.tracks.sha256, trackHash)) 177 177 .execute() 178 - .then(([row]) => row) 178 + .then(([row]) => row), 179 179 ); 180 180 181 181 const generateRkey = Effect.succeed(TID.nextStr()); ··· 184 184 agent: Agent, 185 185 collection: string, 186 186 record: T, 187 - validate: (record: T) => { success: boolean } 187 + validate: (record: T) => { success: boolean }, 188 188 ): Effect.Effect<string, Error> => 189 189 pipe( 190 190 Effect.succeed(record), 191 191 Effect.filterOrFail( 192 192 (rec) => validate(rec).success, 193 - () => new Error("Invalid record") 193 + () => new Error("Invalid record"), 194 194 ), 195 195 Effect.flatMap(() => 196 196 pipe( ··· 203 203 rkey, 204 204 record, 205 205 validate: false, 206 - }) 207 - ) 206 + }), 207 + ), 208 208 ), 209 209 Effect.tap((res) => 210 - Effect.logInfo(`Record created at ${res.data.uri}`) 210 + Effect.logInfo(`Record created at ${res.data.uri}`), 211 211 ), 212 - Effect.map((res) => res.data.uri) 213 - ) 212 + Effect.map((res) => res.data.uri), 213 + ), 214 214 ), 215 215 Effect.catchAll((error) => { 216 216 console.error(`Error creating ${collection} record`, error); 217 217 return Effect.fail(error); 218 - }) 218 + }), 219 219 ); 220 220 221 221 const putArtistRecord = (track: Track, agent: Agent) => ··· 227 227 pictureUrl: track.artistPicture, 228 228 }), 229 229 Effect.flatMap((record) => 230 - putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord) 231 - ) 230 + putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord), 231 + ), 232 232 ); 233 233 234 234 const putAlbumRecord = (track: Track, agent: Agent) => ··· 245 245 albumArtUrl: track.albumArt, 246 246 }), 247 247 Effect.flatMap((record) => 248 - putRecord(agent, "app.rocksky.album", record, Album.validateRecord) 249 - ) 248 + putRecord(agent, "app.rocksky.album", record, Album.validateRecord), 249 + ), 250 250 ); 251 251 252 252 const putSongRecord = (track: Track, agent: Agent) => ··· 272 272 spotifyLink: track.spotifyLink ?? undefined, 273 273 }), 274 274 Effect.flatMap((record) => 275 - putRecord(agent, "app.rocksky.song", record, Song.validateRecord) 276 - ) 275 + putRecord(agent, "app.rocksky.song", record, Song.validateRecord), 276 + ), 277 277 ); 278 278 279 279 const ensureTrack = (ctx: Context, track: Track, agent: Agent) => ··· 288 288 Effect.tap((trackOpt) => 289 289 trackOpt 290 290 ? updateTrackMetadata(ctx, track, trackOpt) 291 - : Effect.succeed(undefined) 291 + : Effect.succeed(undefined), 292 292 ), 293 293 Effect.flatMap((trackOpt) => 294 294 trackOpt.uri 295 295 ? Effect.succeed(trackOpt.uri) 296 - : putSongRecord(track, agent) 297 - ) 298 - ) 299 - ) 300 - ) 301 - ) 296 + : putSongRecord(track, agent), 297 + ), 298 + ), 299 + ), 300 + ), 301 + ), 302 302 ); 303 303 304 304 // Update track metadata (album_uri and artist_uri) 305 305 const updateTrackMetadata = ( 306 306 ctx: Context, 307 307 track: Track, 308 - trackRecord: SelectTrack 308 + trackRecord: SelectTrack, 309 309 ) => 310 310 pipe( 311 311 Effect.succeed(trackRecord), ··· 320 320 .from(tables.albums) 321 321 .where(eq(tables.albums.sha256, albumHash)) 322 322 .execute() 323 - .then(([row]) => row) 324 - ) 323 + .then(([row]) => row), 324 + ), 325 325 ), 326 326 Effect.flatMap((album) => 327 327 Option.fromNullable(album).pipe( ··· 331 331 .update(tables.tracks) 332 332 .set({ albumUri: album.uri }) 333 333 .where(eq(tables.tracks.id, trackRecord.id)) 334 - .execute() 335 - ) 334 + .execute(), 335 + ), 336 336 ), 337 - Effect.catchAll(() => Effect.succeed(undefined)) 338 - ) 339 - ) 337 + Effect.catchAll(() => Effect.succeed(undefined)), 338 + ), 339 + ), 340 340 ) 341 - : Effect.succeed(undefined) 341 + : Effect.succeed(undefined), 342 342 ), 343 343 Effect.tap((trackRecord) => 344 344 !trackRecord.artistUri ··· 351 351 .from(tables.artists) 352 352 .where(eq(tables.artists.sha256, artistHash)) 353 353 .execute() 354 - .then(([row]) => row) 355 - ) 354 + .then(([row]) => row), 355 + ), 356 356 ), 357 357 Effect.flatMap((artist) => 358 358 Option.fromNullable(artist).pipe( ··· 362 362 .update(tables.tracks) 363 363 .set({ artistUri: artist.uri }) 364 364 .where(eq(tables.tracks.id, trackRecord.id)) 365 - .execute() 366 - ) 365 + .execute(), 366 + ), 367 367 ), 368 - Effect.catchAll(() => Effect.succeed(undefined)) 369 - ) 370 - ) 368 + Effect.catchAll(() => Effect.succeed(undefined)), 369 + ), 370 + ), 371 371 ) 372 - : Effect.succeed(undefined) 373 - ) 372 + : Effect.succeed(undefined), 373 + ), 374 374 ); 375 375 376 376 // Ensure artist exists or create it ··· 385 385 .from(tables.artists) 386 386 .where(eq(tables.artists.sha256, artistHash)) 387 387 .execute() 388 - .then(([row]) => row) 388 + .then(([row]) => row), 389 389 ), 390 390 Effect.flatMap((existingArtist) => 391 391 pipe( ··· 393 393 Effect.flatMap((artistOpt) => 394 394 artistOpt.uri 395 395 ? Effect.succeed(artistOpt.uri) 396 - : putArtistRecord(track, agent) 397 - ) 398 - ) 399 - ) 400 - ) 401 - ) 396 + : putArtistRecord(track, agent), 397 + ), 398 + ), 399 + ), 400 + ), 401 + ), 402 402 ); 403 403 404 404 // Ensure album exists or create it ··· 413 413 .from(tables.albums) 414 414 .where(eq(tables.albums.sha256, albumHash)) 415 415 .execute() 416 - .then(([row]) => row) 416 + .then(([row]) => row), 417 417 ), 418 418 Effect.flatMap((existingAlbum) => 419 419 pipe( ··· 421 421 Effect.flatMap((albumOpt) => 422 422 albumOpt.uri 423 423 ? Effect.succeed(albumOpt.uri) 424 - : putAlbumRecord(track, agent) 425 - ) 426 - ) 427 - ) 428 - ) 429 - ) 424 + : putAlbumRecord(track, agent), 425 + ), 426 + ), 427 + ), 428 + ), 429 + ), 430 430 ); 431 431 432 432 // Fetch track, album, and artist by URIs ··· 434 434 ctx: Context, 435 435 trackUri: string, 436 436 albumUri: string, 437 - artistUri: string 437 + artistUri: string, 438 438 ): Effect.Effect< 439 439 { 440 440 track: SelectTrack | null; ··· 450 450 .from(tables.tracks) 451 451 .where(eq(tables.tracks.uri, trackUri)) 452 452 .execute() 453 - .then(([row]) => row) 453 + .then(([row]) => row), 454 454 ), 455 455 album: Effect.tryPromise(() => 456 456 ctx.db ··· 458 458 .from(tables.albums) 459 459 .where(eq(tables.albums.uri, albumUri)) 460 460 .execute() 461 - .then(([row]) => row) 461 + .then(([row]) => row), 462 462 ), 463 463 artist: Effect.tryPromise(() => 464 464 ctx.db ··· 466 466 .from(tables.artists) 467 467 .where(eq(tables.artists.uri, artistUri)) 468 468 .execute() 469 - .then(([row]) => row) 469 + .then(([row]) => row), 470 470 ), 471 471 }); 472 472 ··· 475 475 ctx: Context, 476 476 track: SelectTrack, 477 477 album: SelectAlbum, 478 - artist: SelectArtist 478 + artist: SelectArtist, 479 479 ) => 480 480 pipe( 481 481 Effect.all({ ··· 486 486 .where( 487 487 and( 488 488 eq(tables.albumTracks.albumId, album.id), 489 - eq(tables.albumTracks.trackId, track.id) 490 - ) 489 + eq(tables.albumTracks.trackId, track.id), 490 + ), 491 491 ) 492 492 .execute() 493 - .then(([row]) => row) 493 + .then(([row]) => row), 494 494 ), 495 495 496 496 artistTrack: Effect.tryPromise(() => ··· 500 500 .where( 501 501 and( 502 502 eq(tables.artistTracks.artistId, artist.id), 503 - eq(tables.artistTracks.trackId, track.id) 504 - ) 503 + eq(tables.artistTracks.trackId, track.id), 504 + ), 505 505 ) 506 506 .execute() 507 - .then(([row]) => row) 507 + .then(([row]) => row), 508 508 ), 509 509 artistAlbum: Effect.tryPromise(() => 510 510 ctx.db ··· 513 513 .where( 514 514 and( 515 515 eq(tables.artistAlbums.artistId, artist.id), 516 - eq(tables.artistAlbums.albumId, album.id) 517 - ) 516 + eq(tables.artistAlbums.albumId, album.id), 517 + ), 518 518 ) 519 519 .execute() 520 - .then(([row]) => row) 520 + .then(([row]) => row), 521 521 ), 522 522 }), 523 523 Effect.flatMap(({ albumTrack, artistTrack, artistAlbum }) => ··· 535 535 } as InsertAlbumTrack) 536 536 .returning() 537 537 .execute() 538 - .then(([row]) => row) 539 - ) 540 - ) 538 + .then(([row]) => row), 539 + ), 540 + ), 541 541 ), 542 542 pipe( 543 543 Option.fromNullable(artistTrack), ··· 551 551 } as InsertArtistTrack) 552 552 .returning() 553 553 .execute() 554 - .then(([row]) => row) 555 - ) 556 - ) 554 + .then(([row]) => row), 555 + ), 556 + ), 557 557 ), 558 558 pipe( 559 559 Option.fromNullable(artistAlbum), ··· 567 567 } as InsertArtistAlbum) 568 568 .returning() 569 569 .execute() 570 - .then(([row]) => row) 571 - ) 572 - ) 570 + .then(([row]) => row), 571 + ), 572 + ), 573 573 ), 574 574 ]), 575 575 Effect.map(([albumTrack, artistTrack, artistAlbum]) => ({ 576 576 albumTrack, 577 577 artistTrack, 578 578 artistAlbum, 579 - })) 580 - ) 581 - ) 579 + })), 580 + ), 581 + ), 582 582 ); 583 583 584 584 // Update track with album and artist URIs if missing ··· 586 586 ctx: Context, 587 587 track: SelectTrack, 588 588 album: SelectAlbum, 589 - artist: SelectArtist 589 + artist: SelectArtist, 590 590 ) => 591 591 pipe( 592 592 Effect.succeed(track), ··· 599 599 albumUri: album.uri, 600 600 }) 601 601 .where(eq(tables.tracks.id, trackRecord.id)) 602 - .execute() 602 + .execute(), 603 603 ) 604 - : Effect.succeed(undefined) 604 + : Effect.succeed(undefined), 605 605 ), 606 606 Effect.tap((trackRecord) => 607 607 !trackRecord.artistUri ··· 612 612 artistUri: artist.uri, 613 613 }) 614 614 .where(eq(tables.tracks.id, trackRecord.id)) 615 - .execute() 615 + .execute(), 616 616 ) 617 - : Effect.succeed(undefined) 618 - ) 617 + : Effect.succeed(undefined), 618 + ), 619 619 ); 620 620 621 621 const publishTrack = ( ··· 623 623 track: SelectTrack, 624 624 albumTrack: SelectAlbumTrack, 625 625 artistTrack: SelectArtistTrack, 626 - artistAlbum: SelectArtistAlbum 626 + artistAlbum: SelectArtistAlbum, 627 627 ) => 628 628 pipe( 629 629 Effect.succeed( ··· 673 673 xata_updatedat: artistAlbum.updatedAt.toISOString(), 674 674 xata_version: artistAlbum.xataVersion, 675 675 }, 676 - }) 676 + }), 677 677 ), 678 678 Effect.flatMap((message) => 679 679 Effect.try(() => 680 680 ctx.nc.publish( 681 681 "rocksky.track", 682 - Buffer.from(JSON.stringify(message).replaceAll("sha_256", "sha256")) 683 - ) 684 - ) 685 - ) 682 + Buffer.from(JSON.stringify(message).replaceAll("sha_256", "sha256")), 683 + ), 684 + ), 685 + ), 686 686 ); 687 687 688 688 export const saveTrack = (ctx: Context, track: Track, agent: Agent) => ··· 735 735 Effect.filterOrFail( 736 736 () => !!track, 737 737 () => 738 - new Error(`Track not found for uri: ${trackUri}`) 739 - ) 738 + new Error(`Track not found for uri: ${trackUri}`), 739 + ), 740 740 ), 741 741 Option.fromNullable(album).pipe( 742 742 Effect.filterOrFail( 743 743 () => !!album, 744 744 () => 745 - new Error(`Album not found for uri: ${albumUri}`) 746 - ) 745 + new Error(`Album not found for uri: ${albumUri}`), 746 + ), 747 747 ), 748 748 Option.fromNullable(artist).pipe( 749 749 Effect.filterOrFail( 750 750 () => !!artist, 751 751 () => 752 - new Error(`Artist not found for uri: ${artistUri}`) 753 - ) 752 + new Error(`Artist not found for uri: ${artistUri}`), 753 + ), 754 754 ), 755 755 ]), 756 756 Effect.flatMap(([track, album, artist]) => 757 757 pipe( 758 758 updateTrackUris(ctx, track, album, artist), 759 759 Effect.flatMap(() => 760 - ensureRelationships(ctx, track, album, artist) 760 + ensureRelationships(ctx, track, album, artist), 761 761 ), 762 762 Effect.map( 763 763 ({ albumTrack, artistTrack, artistAlbum }) => ({ ··· 768 768 albumTrack, 769 769 artistTrack, 770 770 artistAlbum, 771 - }) 772 - ) 773 - ) 774 - ) 775 - ) 771 + }), 772 + ), 773 + ), 774 + ), 775 + ), 776 776 ), 777 777 Effect.tap( 778 778 ({ ··· 794 794 track.albumUri && 795 795 track.artistUri 796 796 ? `Track saved successfully after ${chalk.magenta(tries + 1)} tries` 797 - : `Track not yet saved, retrying... ${chalk.magenta(tries + 1)}` 798 - ) 797 + : `Track not yet saved, retrying... ${chalk.magenta(tries + 1)}`, 798 + ), 799 799 ), 800 800 Effect.tap( 801 801 ({ ··· 810 810 tries === 15 811 811 ? pipe( 812 812 Effect.logError( 813 - "Failed to save track after 15 tries" 813 + "Failed to save track after 15 tries", 814 814 ), 815 815 Effect.tap(() => 816 816 Effect.logDebug( 817 - `Debug info: track=${JSON.stringify(track)}, album=${JSON.stringify(album)}, artist=${JSON.stringify(artist)}, albumTrack=${JSON.stringify(albumTrack)}, artistTrack=${JSON.stringify(artistTrack)}, artistAlbum=${JSON.stringify(artistAlbum)}` 818 - ) 819 - ) 817 + `Debug info: track=${JSON.stringify(track)}, album=${JSON.stringify(album)}, artist=${JSON.stringify(artist)}, albumTrack=${JSON.stringify(albumTrack)}, artistTrack=${JSON.stringify(artistTrack)}, artistAlbum=${JSON.stringify(artistAlbum)}`, 818 + ), 819 + ), 820 820 ) 821 - : Effect.succeed(undefined) 821 + : Effect.succeed(undefined), 822 822 ), 823 - Effect.delay("1 second") 823 + Effect.delay("1 second"), 824 824 ), 825 - } 825 + }, 826 826 ), 827 827 Effect.tap(({ tries, track, albumTrack, artistTrack, artistAlbum }) => 828 828 tries < 15 && track && albumTrack && artistTrack && artistAlbum 829 829 ? publishTrack(ctx, track, albumTrack, artistTrack, artistAlbum) 830 - : Effect.succeed(undefined) 831 - ) 832 - ) 833 - ) 830 + : Effect.succeed(undefined), 831 + ), 832 + ), 833 + ), 834 834 );