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: clean up createSong.ts by removing unused imports and simplifying error handling

- Removed unused BlobRef import and downloadImage function.
- Simplified error handling in various Effect.catchAll calls.
- Updated formatting for consistency and readability.
- Adjusted object structures for artist and album records to use pictureUrl and albumArtUrl.
- Streamlined the ensureTrack, ensureArtist, and ensureAlbum functions for clarity.

+332 -492
+6 -67
apps/api/src/nowplaying/nowplaying.service.ts
··· 1 - import type { Agent, BlobRef } from "@atproto/api"; 1 + import type { Agent } from "@atproto/api"; 2 2 import { TID } from "@atproto/common"; 3 3 import { equals } from "@xata.io/client"; 4 4 import chalk from "chalk"; ··· 8 8 import * as Artist from "lexicon/types/app/rocksky/artist"; 9 9 import * as Scrobble from "lexicon/types/app/rocksky/scrobble"; 10 10 import * as Song from "lexicon/types/app/rocksky/song"; 11 - import downloadImage, { getContentType } from "lib/downloadImage"; 12 11 import { createHash } from "node:crypto"; 13 12 import type { Track } from "types/track"; 14 13 ··· 21 20 $type: string; 22 21 name: string; 23 22 createdAt: string; 24 - picture?: BlobRef; 23 + pictureUrl?: string; 25 24 tags?: string[]; 26 25 } = { 27 26 $type: "app.rocksky.artist", 28 27 name: track.albumArtist, 29 28 createdAt: new Date().toISOString(), 29 + pictureUrl: track.artistPicture, 30 30 tags: track.genres, 31 31 }; 32 32 33 - if (track.artistPicture) { 34 - const imageBuffer = await downloadImage(track.artistPicture); 35 - const encoding = await getContentType(track.artistPicture); 36 - const uploadResponse = await agent.uploadBlob(imageBuffer, { 37 - encoding, 38 - }); 39 - record.picture = uploadResponse.data.blob; 40 - } 41 - 42 33 if (!Artist.validateRecord(record).success) { 43 34 console.log(Artist.validateRecord(record)); 44 35 throw new Error("Invalid record"); ··· 66 57 agent: Agent 67 58 ): Promise<string | null> { 68 59 const rkey = TID.nextStr(); 69 - let albumArt; 70 - 71 - if (track.albumArt) { 72 - let options; 73 - if (track.albumArt.endsWith(".jpeg") || track.albumArt.endsWith(".jpg")) { 74 - options = { encoding: "image/jpeg" }; 75 - } 76 - 77 - if (track.albumArt.endsWith(".png")) { 78 - options = { encoding: "image/png" }; 79 - } 80 - 81 - if (!options?.encoding) { 82 - options = { encoding: await getContentType(track.albumArt) }; 83 - } 84 - 85 - const imageBuffer = await downloadImage(track.albumArt); 86 - const uploadResponse = await agent.uploadBlob(imageBuffer, options); 87 - albumArt = uploadResponse.data.blob; 88 - } 89 60 90 61 const record = { 91 62 $type: "app.rocksky.album", ··· 96 67 ? track.releaseDate.toISOString() 97 68 : undefined, 98 69 createdAt: new Date().toISOString(), 99 - albumArt, 70 + albumArtUrl: track.albumArt, 100 71 }; 101 72 102 73 if (!Album.validateRecord(record).success) { ··· 126 97 agent: Agent 127 98 ): Promise<string | null> { 128 99 const rkey = TID.nextStr(); 129 - let albumArt; 130 - 131 - if (track.albumArt) { 132 - let options; 133 - if (track.albumArt.endsWith(".jpeg") || track.albumArt.endsWith(".jpg")) { 134 - options = { encoding: "image/jpeg" }; 135 - } 136 - 137 - if (track.albumArt.endsWith(".png")) { 138 - options = { encoding: "image/png" }; 139 - } 140 - 141 - const imageBuffer = await downloadImage(track.albumArt); 142 - const uploadResponse = await agent.uploadBlob(imageBuffer, options); 143 - albumArt = uploadResponse.data.blob; 144 - } 145 100 146 101 const record = { 147 102 $type: "app.rocksky.song", ··· 154 109 ? track.releaseDate.toISOString() 155 110 : undefined, 156 111 year: track.year, 157 - albumArt, 112 + albumArtUrl: track.albumArt, 158 113 composer: track.composer ? track.composer : undefined, 159 114 lyrics: track.lyrics ? track.lyrics : undefined, 160 115 trackNumber: track.trackNumber, ··· 194 149 agent: Agent 195 150 ): Promise<string | null> { 196 151 const rkey = TID.nextStr(); 197 - let albumArt; 198 - 199 - if (track.albumArt) { 200 - let options; 201 - if (track.albumArt.endsWith(".jpeg") || track.albumArt.endsWith(".jpg")) { 202 - options = { encoding: "image/jpeg" }; 203 - } 204 - 205 - if (track.albumArt.endsWith(".png")) { 206 - options = { encoding: "image/png" }; 207 - } 208 - 209 - const imageBuffer = await downloadImage(track.albumArt); 210 - const uploadResponse = await agent.uploadBlob(imageBuffer, options); 211 - albumArt = uploadResponse.data.blob; 212 - } 213 152 214 153 const record = { 215 154 $type: "app.rocksky.scrobble", 216 155 title: track.title, 217 156 albumArtist: track.albumArtist, 218 - albumArt, 157 + albumArtUrl: track.albumArt, 219 158 artist: track.artist, 220 159 album: track.album, 221 160 duration: track.duration,
+6 -16
apps/api/src/subscribers/playlist.ts
··· 1 1 import { TID } from "@atproto/common"; 2 - import type { BlobRef } from "@atproto/lexicon"; 3 2 import chalk from "chalk"; 4 3 import type { Context } from "context"; 5 4 import { eq } from "drizzle-orm"; 6 5 import * as Playlist from "lexicon/types/app/rocksky/playlist"; 7 6 import { createAgent } from "lib/agent"; 8 - import downloadImage, { getContentType } from "lib/downloadImage"; 9 7 import { StringCodec } from "nats"; 10 8 import tables from "schema"; 11 9 ··· 19 17 did: string; 20 18 } = JSON.parse(sc.decode(m.data)); 21 19 console.log( 22 - `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}`, 20 + `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}` 23 21 ); 24 22 await putPlaylistRecord(ctx, payload); 25 23 } ··· 28 26 29 27 async function putPlaylistRecord( 30 28 ctx: Context, 31 - payload: { id: string; did: string }, 29 + payload: { id: string; did: string } 32 30 ) { 33 31 const agent = await createAgent(ctx.oauthClient, payload.did); 34 32 35 33 if (!agent) { 36 34 console.error( 37 - `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)}` 38 36 ); 39 37 return; 40 38 } ··· 56 54 name: string; 57 55 description?: string; 58 56 createdAt: string; 59 - picture?: BlobRef; 57 + pictureUrl?: string; 60 58 spotifyLink?: string; 61 59 tidalLink?: string; 62 60 appleMusicLink?: string; ··· 66 64 name: playlist.name, 67 65 description: playlist.description, 68 66 createdAt: new Date().toISOString(), 67 + pictureUrl: playlist.picture, 69 68 spotifyLink: playlist.spotifyLink, 70 69 }; 71 70 72 - if (playlist.picture) { 73 - const imageBuffer = await downloadImage(playlist.picture); 74 - const encoding = await getContentType(playlist.picture); 75 - const uploadResponse = await agent.uploadBlob(imageBuffer, { 76 - encoding, 77 - }); 78 - record.picture = uploadResponse.data.blob; 79 - } 80 - 81 71 if (!Playlist.validateRecord(record)) { 82 72 console.error(`Invalid record: ${chalk.redBright(JSON.stringify(record))}`); 83 73 return; ··· 110 100 111 101 await ctx.meilisearch.post( 112 102 `indexes/playlists/documents?primaryKey=id`, 113 - updatedPlaylist, 103 + updatedPlaylist 114 104 ); 115 105 }
+186 -234
apps/api/src/xrpc/app/rocksky/scrobble/createScrobble.ts
··· 1 - import type { Agent, BlobRef } from "@atproto/api"; 1 + import type { Agent } from "@atproto/api"; 2 2 import { TID } from "@atproto/common"; 3 3 import type { HandlerAuth } from "@atproto/xrpc-server"; 4 4 import chalk from "chalk"; ··· 15 15 import * as Song from "lexicon/types/app/rocksky/song"; 16 16 import { deepSnakeCaseKeys } from "lib"; 17 17 import { createAgent } from "lib/agent"; 18 - import downloadImage from "lib/downloadImage"; 19 18 import { createHash } from "node:crypto"; 20 19 import tables from "schema"; 21 20 import type { SelectAlbum } from "schema/albums"; ··· 38 37 pipe( 39 38 scrobbleTrack(ctx, track, agent, did), 40 39 Effect.tap(() => 41 - Effect.logInfo(`Scrobble created for ${chalk.cyan(track.title)}`), 42 - ), 43 - ), 40 + Effect.logInfo(`Scrobble created for ${chalk.cyan(track.title)}`) 41 + ) 42 + ) 44 43 ), 45 44 Effect.flatMap(presentation), 46 45 Effect.retry({ times: 3 }), ··· 48 47 Effect.catchAll((err) => { 49 48 console.error(err); 50 49 return Effect.succeed({}); 51 - }), 50 + }) 52 51 ); 53 52 server.app.rocksky.scrobble.createScrobble({ 54 53 auth: ctx.authVerifier, ··· 82 81 ctx, 83 82 did, 84 83 input, 85 - })), 84 + })) 86 85 ), 87 86 Match.orElse(() => { 88 87 throw new Error("Authentication required to create a scrobble"); 89 - }), 88 + }) 90 89 ), 91 90 catch: (error) => new Error(`Failed to create agent: ${error}`), 92 91 }); ··· 121 120 122 121 const generateRkey = Effect.succeed(TID.nextStr()); 123 122 124 - const uploadImage = (url: string, agent: Agent) => 125 - pipe( 126 - Effect.tryPromise(() => downloadImage(url)), 127 - Effect.map< 128 - Buffer<ArrayBufferLike>, 129 - [Buffer<ArrayBufferLike>, { encoding: string } | undefined] 130 - >((imageBuffer) => { 131 - if (url.endsWith(".jpeg") || url.endsWith(".jpg")) { 132 - return [imageBuffer, { encoding: "image/jpeg" }]; 133 - } else if (url.endsWith(".png")) { 134 - return [imageBuffer, { encoding: "image/png" }]; 135 - } 136 - return [imageBuffer, undefined]; 137 - }), 138 - Effect.flatMap(([imageBuffer, options]) => 139 - pipe( 140 - Effect.tryPromise(() => agent.uploadBlob(imageBuffer, options)), 141 - Effect.map((uploadResponse) => uploadResponse.data.blob), 142 - ), 143 - ), 144 - Effect.catchAll(() => Effect.succeed(undefined as BlobRef | undefined)), 145 - ); 146 - 147 123 const putRecord = <T>( 148 124 agent: Agent, 149 125 collection: string, 150 126 record: T, 151 - validate: (record: T) => { success: boolean }, 127 + validate: (record: T) => { success: boolean } 152 128 ) => 153 129 pipe( 154 130 Effect.succeed(record), 155 131 Effect.filterOrFail( 156 132 (rec) => validate(rec).success, 157 - () => new Error("Invalid record"), 133 + () => new Error("Invalid record") 158 134 ), 159 135 Effect.flatMap(() => 160 136 pipe( ··· 167 143 rkey, 168 144 record, 169 145 validate: false, 170 - }), 171 - ), 146 + }) 147 + ) 172 148 ), 173 149 Effect.tap((res) => 174 - Effect.logInfo(`Record created at ${res.data.uri}`), 150 + Effect.logInfo(`Record created at ${res.data.uri}`) 175 151 ), 176 - Effect.map((res) => res.data.uri), 177 - ), 152 + Effect.map((res) => res.data.uri) 153 + ) 178 154 ), 179 155 Effect.catchAll((error) => { 180 156 console.error(`Error creating ${collection} record`, error); 181 157 return Effect.succeed(null); 182 - }), 158 + }) 183 159 ); 184 160 185 161 const putArtistRecord = (track: Track, agent: Agent) => 186 162 pipe( 187 - track.artistPicture 188 - ? uploadImage(track.artistPicture, agent) 189 - : Effect.succeed(undefined), 190 - Effect.map((picture) => ({ 163 + Effect.succeed({ 191 164 $type: "app.rocksky.artist", 192 165 name: track.albumArtist, 193 166 createdAt: new Date().toISOString(), 194 - picture, 167 + pictureUrl: track.artistPicture, 195 168 tags: track.genres, 196 - })), 169 + }), 197 170 Effect.flatMap((record) => 198 - putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord), 199 - ), 171 + putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord) 172 + ) 200 173 ); 201 174 202 175 const putAlbumRecord = (track: Track, agent: Agent) => 203 176 pipe( 204 - Match.value(track.albumArt).pipe( 205 - Match.when( 206 - (url) => !!url, 207 - (url) => uploadImage(url, agent), 208 - ), 209 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 210 - ), 211 - Effect.map((albumArt) => ({ 177 + Effect.succeed({ 212 178 $type: "app.rocksky.album", 213 179 title: track.album, 214 180 artist: track.albumArtist, ··· 217 183 ? track.releaseDate.toISOString() 218 184 : undefined, 219 185 createdAt: new Date().toISOString(), 220 - albumArt, 221 - })), 186 + albumArtUrl: track.albumArt, 187 + }), 222 188 Effect.flatMap((record) => 223 - putRecord(agent, "app.rocksky.album", record, Album.validateRecord), 224 - ), 189 + putRecord(agent, "app.rocksky.album", record, Album.validateRecord) 190 + ) 225 191 ); 226 192 227 193 const putSongRecord = (track: Track, agent: Agent) => 228 194 pipe( 229 - Match.value(track.albumArt).pipe( 230 - Match.when( 231 - (url) => !!url, 232 - (url) => uploadImage(url, agent), 233 - ), 234 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 235 - ), 236 - Effect.map((albumArt) => ({ 195 + Effect.succeed({ 237 196 $type: "app.rocksky.song", 238 197 title: track.title, 239 198 artist: track.artist, ··· 244 203 ? track.releaseDate.toISOString() 245 204 : undefined, 246 205 year: track.year, 247 - albumArt, 206 + albumArtUrl: track.albumArt, 248 207 composer: track.composer ?? undefined, 249 208 lyrics: track.lyrics ?? undefined, 250 209 trackNumber: track.trackNumber, ··· 252 211 copyrightMessage: track.copyrightMessage ?? undefined, 253 212 createdAt: new Date().toISOString(), 254 213 spotifyLink: track.spotifyLink ?? undefined, 255 - })), 214 + }), 256 215 Effect.flatMap((record) => 257 - putRecord(agent, "app.rocksky.song", record, Song.validateRecord), 258 - ), 216 + putRecord(agent, "app.rocksky.song", record, Song.validateRecord) 217 + ) 259 218 ); 260 219 261 220 const putScrobbleRecord = (track: Track, agent: Agent) => 262 221 pipe( 263 - Match.value(track.albumArt).pipe( 264 - Match.when( 265 - (url) => !!url, 266 - (url) => uploadImage(url, agent), 267 - ), 268 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 269 - ), 270 - Effect.map((albumArt) => ({ 222 + Effect.succeed({ 271 223 $type: "app.rocksky.scrobble", 272 224 title: track.title, 273 225 albumArtist: track.albumArtist, 274 - albumArt, 226 + albumArtUrl: track.albumArt, 275 227 artist: track.artist, 276 228 album: track.album, 277 229 duration: track.duration, ··· 288 240 ? dayjs.unix(track.timestamp).toISOString() 289 241 : new Date().toISOString(), 290 242 spotifyLink: track.spotifyLink ?? undefined, 291 - })), 243 + }), 292 244 Effect.flatMap((record) => 293 - putRecord(agent, "app.rocksky.scrobble", record, Scrobble.validateRecord), 294 - ), 245 + putRecord(agent, "app.rocksky.scrobble", record, Scrobble.validateRecord) 246 + ) 295 247 ); 296 248 297 249 const getScrobble = ({ ctx, id }: { ctx: Context; id: string }) => ··· 303 255 .leftJoin(tables.albums, eq(tables.albums.id, tables.scrobbles.albumId)) 304 256 .leftJoin( 305 257 tables.artists, 306 - eq(tables.artists.id, tables.scrobbles.artistId), 258 + eq(tables.artists.id, tables.scrobbles.artistId) 307 259 ) 308 260 .leftJoin(tables.users, eq(tables.users.id, tables.scrobbles.userId)) 309 261 .where(eq(tables.scrobbles.id, id)) 310 262 .execute() 311 - .then(([row]) => row), 263 + .then(([row]) => row) 312 264 ); 313 265 314 266 const getUserAlbum = ( ··· 318 270 artists: SelectArtist; 319 271 users: SelectUser; 320 272 tracks: SelectTrack; 321 - }, 273 + } 322 274 ) => 323 275 Effect.tryPromise(() => 324 276 ctx.db ··· 326 278 .from(tables.userAlbums) 327 279 .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 328 280 .execute() 329 - .then(([row]) => row), 281 + .then(([row]) => row) 330 282 ); 331 283 332 284 const getUserArtist = ( ··· 336 288 artists: SelectArtist; 337 289 users: SelectUser; 338 290 tracks: SelectTrack; 339 - }, 291 + } 340 292 ) => 341 293 Effect.tryPromise(() => 342 294 ctx.db ··· 344 296 .from(tables.userArtists) 345 297 .where(eq(tables.userArtists.id, scrobble.artists.id)) 346 298 .execute() 347 - .then(([row]) => row), 299 + .then(([row]) => row) 348 300 ); 349 301 350 302 const getUserTrack = ( ··· 354 306 artists: SelectArtist; 355 307 users: SelectUser; 356 308 tracks: SelectTrack; 357 - }, 309 + } 358 310 ) => 359 311 Effect.tryPromise(() => 360 312 ctx.db ··· 362 314 .from(tables.userTracks) 363 315 .where(eq(tables.userTracks.id, scrobble.tracks.id)) 364 316 .execute() 365 - .then(([row]) => row), 317 + .then(([row]) => row) 366 318 ); 367 319 368 320 const getAlbumTrack = ( ··· 372 324 artists: SelectArtist; 373 325 users: SelectUser; 374 326 tracks: SelectTrack; 375 - }, 327 + } 376 328 ) => 377 329 Effect.tryPromise(() => 378 330 ctx.db ··· 380 332 .from(tables.albumTracks) 381 333 .where(eq(tables.albumTracks.trackId, scrobble.tracks.id)) 382 334 .execute() 383 - .then(([row]) => row), 335 + .then(([row]) => row) 384 336 ); 385 337 386 338 const getArtistTrack = ( ··· 390 342 artists: SelectArtist; 391 343 users: SelectUser; 392 344 tracks: SelectTrack; 393 - }, 345 + } 394 346 ) => 395 347 Effect.tryPromise(() => 396 348 ctx.db ··· 398 350 .from(tables.artistTracks) 399 351 .where(eq(tables.artistTracks.trackId, scrobble.tracks.id)) 400 352 .execute() 401 - .then(([row]) => row), 353 + .then(([row]) => row) 402 354 ); 403 355 404 356 const getArtistAlbum = ( ··· 408 360 artists: SelectArtist; 409 361 users: SelectUser; 410 362 tracks: SelectTrack; 411 - }, 363 + } 412 364 ) => 413 365 Effect.tryPromise(() => 414 366 ctx.db ··· 417 369 .where( 418 370 and( 419 371 eq(tables.artistAlbums.albumId, scrobble.albums.id), 420 - eq(tables.artistAlbums.artistId, scrobble.artists.id), 421 - ), 372 + eq(tables.artistAlbums.artistId, scrobble.artists.id) 373 + ) 422 374 ) 423 - .then(([row]) => row), 375 + .then(([row]) => row) 424 376 ); 425 377 426 378 const createUserArtist = ( ··· 430 382 artists: SelectArtist; 431 383 users: SelectUser; 432 384 tracks: SelectTrack; 433 - }, 385 + } 434 386 ) => 435 387 pipe( 436 388 Effect.tryPromise(() => ··· 442 394 uri: scrobble.artists.uri, 443 395 scrobbles: 1, 444 396 } as InsertUserArtist) 445 - .execute(), 397 + .execute() 446 398 ), 447 399 Effect.flatMap(() => 448 400 Effect.tryPromise(() => ··· 451 403 .from(tables.userArtists) 452 404 .where(eq(tables.userArtists.artistId, scrobble.artists.id)) 453 405 .execute() 454 - .then(([row]) => row), 455 - ), 456 - ), 406 + .then(([row]) => row) 407 + ) 408 + ) 457 409 ); 458 410 459 411 const createUserAlbum = ( ··· 463 415 artists: SelectArtist; 464 416 users: SelectUser; 465 417 tracks: SelectTrack; 466 - }, 418 + } 467 419 ) => 468 420 pipe( 469 421 Effect.tryPromise(() => ··· 475 427 uri: scrobble.albums.uri, 476 428 scrobbles: 1, 477 429 } as InsertUserAlbum) 478 - .execute(), 430 + .execute() 479 431 ), 480 432 Effect.flatMap(() => 481 433 Effect.tryPromise(() => ··· 484 436 .from(tables.userAlbums) 485 437 .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 486 438 .execute() 487 - .then(([row]) => row), 488 - ), 489 - ), 439 + .then(([row]) => row) 440 + ) 441 + ) 490 442 ); 491 443 492 444 const createUserTrack = ( ··· 496 448 artists: SelectArtist; 497 449 users: SelectUser; 498 450 tracks: SelectTrack; 499 - }, 451 + } 500 452 ) => 501 453 pipe( 502 454 Effect.tryPromise(() => ··· 508 460 uri: scrobble.tracks.uri, 509 461 scrobbles: 1, 510 462 } as InsertUserTrack) 511 - .execute(), 463 + .execute() 512 464 ), 513 465 Effect.flatMap(() => 514 466 Effect.tryPromise(() => ··· 516 468 .select() 517 469 .from(tables.userTracks) 518 470 .where(eq(tables.userTracks.trackId, scrobble.tracks.id)) 519 - .then(([row]) => row), 520 - ), 521 - ), 471 + .then(([row]) => row) 472 + ) 473 + ) 522 474 ); 523 475 524 476 const publishScrobble = (ctx: Context, id: string) => ··· 689 641 xata_updatedat: artistAlbum.updatedAt.toISOString(), 690 642 xata_version: artistAlbum.xataVersion, 691 643 }, 692 - }), 693 - ), 694 - ), 695 - ), 696 - ), 644 + }) 645 + ) 646 + ) 647 + ) 648 + ) 697 649 ), 698 650 Effect.flatMap((data) => 699 651 Effect.try(() => 700 652 ctx.nc.publish( 701 653 "rocksky.scrobble", 702 654 Buffer.from( 703 - JSON.stringify(data).replaceAll("sha_256", "sha256"), 704 - ), 705 - ), 706 - ), 707 - ), 708 - ), 709 - ), 710 - ), 711 - ), 655 + JSON.stringify(data).replaceAll("sha_256", "sha256") 656 + ) 657 + ) 658 + ) 659 + ) 660 + ) 661 + ) 662 + ) 663 + ) 712 664 ); 713 665 714 666 const computeTrackHash = (track: Track): Effect.Effect<string, never> => 715 667 Effect.succeed( 716 668 createHash("sha256") 717 669 .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) 718 - .digest("hex"), 670 + .digest("hex") 719 671 ); 720 672 721 673 const computeAlbumHash = (track: Track): Effect.Effect<string, never> => 722 674 Effect.succeed( 723 675 createHash("sha256") 724 676 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 725 - .digest("hex"), 677 + .digest("hex") 726 678 ); 727 679 728 680 const computeArtistHash = (track: Track): Effect.Effect<string, never> => 729 681 Effect.succeed( 730 - createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex"), 682 + createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex") 731 683 ); 732 684 733 685 const fetchExistingTrack = ( 734 686 ctx: Context, 735 - trackHash: string, 687 + trackHash: string 736 688 ): Effect.Effect<SelectTrack | undefined, Error> => 737 689 Effect.tryPromise(() => 738 690 ctx.db ··· 740 692 .from(tables.tracks) 741 693 .where(eq(tables.tracks.sha256, trackHash)) 742 694 .execute() 743 - .then(([row]) => row), 695 + .then(([row]) => row) 744 696 ); 745 697 746 698 // Update track metadata (album_uri and artist_uri) 747 699 const updateTrackMetadata = ( 748 700 ctx: Context, 749 701 track: Track, 750 - trackRecord: SelectTrack, 702 + trackRecord: SelectTrack 751 703 ) => 752 704 pipe( 753 705 Effect.succeed(trackRecord), ··· 762 714 .from(tables.albums) 763 715 .where(eq(tables.albums.sha256, albumHash)) 764 716 .execute() 765 - .then(([row]) => row), 766 - ), 717 + .then(([row]) => row) 718 + ) 767 719 ), 768 720 Effect.flatMap((album) => 769 721 album ··· 774 726 albumUri: album.uri, 775 727 }) 776 728 .where(eq(tables.tracks.id, trackRecord.id)) 777 - .execute(), 729 + .execute() 778 730 ) 779 - : Effect.succeed(undefined), 780 - ), 731 + : Effect.succeed(undefined) 732 + ) 781 733 ) 782 - : Effect.succeed(undefined), 734 + : Effect.succeed(undefined) 783 735 ), 784 736 Effect.tap((trackRecord) => 785 737 !trackRecord.artistUri ··· 792 744 .from(tables.artists) 793 745 .where(eq(tables.artists.sha256, artistHash)) 794 746 .execute() 795 - .then(([row]) => row), 796 - ), 747 + .then(([row]) => row) 748 + ) 797 749 ), 798 750 Effect.flatMap((artist) => 799 751 artist ··· 804 756 artistUri: artist.uri, 805 757 }) 806 758 .where(eq(tables.tracks.id, trackRecord.id)) 807 - .execute(), 759 + .execute() 808 760 ) 809 - : Effect.succeed(undefined), 810 - ), 761 + : Effect.succeed(undefined) 762 + ) 811 763 ) 812 - : Effect.succeed(undefined), 813 - ), 764 + : Effect.succeed(undefined) 765 + ) 814 766 ); 815 767 816 768 // Ensure track exists or create it ··· 819 771 track: Track, 820 772 agent: Agent, 821 773 userDid: string, 822 - existingTrack: SelectTrack | undefined, 774 + existingTrack: SelectTrack | undefined 823 775 ) => 824 776 pipe( 825 777 Effect.succeed(existingTrack), ··· 827 779 Match.value(trackOpt).pipe( 828 780 Match.when( 829 781 (value) => !!value, 830 - () => updateTrackMetadata(ctx, track, trackOpt), 782 + () => updateTrackMetadata(ctx, track, trackOpt) 831 783 ), 832 - Match.orElse(() => Effect.succeed(undefined)), 833 - ), 784 + Match.orElse(() => Effect.succeed(undefined)) 785 + ) 834 786 ), 835 787 Effect.flatMap((trackOpt) => 836 788 pipe( ··· 840 792 .from(tables.userTracks) 841 793 .leftJoin( 842 794 tables.tracks, 843 - eq(tables.userTracks.trackId, tables.tracks.id), 795 + eq(tables.userTracks.trackId, tables.tracks.id) 844 796 ) 845 797 .leftJoin( 846 798 tables.users, 847 - eq(tables.userTracks.userId, tables.users.id), 799 + eq(tables.userTracks.userId, tables.users.id) 848 800 ) 849 801 .where( 850 802 and( 851 803 eq(tables.tracks.id, trackOpt?.id), 852 - eq(tables.users.did, userDid), 853 - ), 804 + eq(tables.users.did, userDid) 805 + ) 854 806 ) 855 807 .execute() 856 - .then(([row]) => row.user_tracks), 808 + .then(([row]) => row.user_tracks) 857 809 ), 858 810 Effect.flatMap((userTrack) => 859 811 Option.isNone(Option.fromNullable(userTrack)) || 860 812 !userTrack?.uri?.includes(userDid) 861 813 ? putSongRecord(track, agent) 862 - : Effect.succeed(null), 863 - ), 864 - ), 865 - ), 814 + : Effect.succeed(null) 815 + ) 816 + ) 817 + ) 866 818 ); 867 819 868 820 // Ensure album exists or create it ··· 870 822 ctx: Context, 871 823 track: Track, 872 824 agent: Agent, 873 - userDid: string, 825 + userDid: string 874 826 ) => 875 827 pipe( 876 828 computeAlbumHash(track), ··· 881 833 .from(tables.albums) 882 834 .where(eq(tables.albums.sha256, albumHash)) 883 835 .execute() 884 - .then(([row]) => row), 885 - ), 836 + .then(([row]) => row) 837 + ) 886 838 ), 887 839 Effect.flatMap((existingAlbum) => 888 840 pipe( ··· 894 846 .from(tables.userAlbums) 895 847 .leftJoin( 896 848 tables.albums, 897 - eq(tables.userAlbums.albumId, tables.albums.id), 849 + eq(tables.userAlbums.albumId, tables.albums.id) 898 850 ) 899 851 .leftJoin( 900 852 tables.users, 901 - eq(tables.userAlbums.userId, tables.users.id), 853 + eq(tables.userAlbums.userId, tables.users.id) 902 854 ) 903 855 .where( 904 856 and( 905 857 eq(tables.albums.id, album.id), 906 - eq(tables.users.did, userDid), 907 - ), 858 + eq(tables.users.did, userDid) 859 + ) 908 860 ) 909 861 .execute() 910 - .then(([row]) => row.user_albums), 911 - ), 862 + .then(([row]) => row.user_albums) 863 + ) 912 864 ), 913 865 Effect.flatMap((userAlbum) => 914 866 Option.isNone(Option.fromNullable(existingAlbum)) || 915 867 Option.isNone(Option.fromNullable(userAlbum)) || 916 868 !userAlbum?.uri?.includes(userDid) 917 869 ? putAlbumRecord(track, agent) 918 - : Effect.succeed(null), 919 - ), 920 - ), 921 - ), 870 + : Effect.succeed(null) 871 + ) 872 + ) 873 + ) 922 874 ); 923 875 924 876 // Ensure artist exists or create it ··· 926 878 ctx: Context, 927 879 track: Track, 928 880 agent: Agent, 929 - userDid: string, 881 + userDid: string 930 882 ) => 931 883 pipe( 932 884 computeArtistHash(track), ··· 937 889 .from(tables.artists) 938 890 .where(eq(tables.artists.sha256, artistHash)) 939 891 .execute() 940 - .then(([row]) => row), 941 - ), 892 + .then(([row]) => row) 893 + ) 942 894 ), 943 895 Effect.flatMap((existingArtist) => 944 896 pipe( ··· 950 902 .from(tables.userArtists) 951 903 .leftJoin( 952 904 tables.artists, 953 - eq(tables.userArtists.artistId, tables.artists.id), 905 + eq(tables.userArtists.artistId, tables.artists.id) 954 906 ) 955 907 .leftJoin( 956 908 tables.users, 957 - eq(tables.userArtists.userId, tables.users.id), 909 + eq(tables.userArtists.userId, tables.users.id) 958 910 ) 959 911 .where( 960 912 and( 961 913 eq(tables.artists.id, artist.id), 962 - eq(tables.users.did, userDid), 963 - ), 914 + eq(tables.users.did, userDid) 915 + ) 964 916 ) 965 917 .execute() 966 - .then(([row]) => row.user_artists), 967 - ), 918 + .then(([row]) => row.user_artists) 919 + ) 968 920 ), 969 921 Effect.flatMap((userArtist) => 970 922 Effect.if( ··· 974 926 { 975 927 onTrue: () => putArtistRecord(track, agent), 976 928 onFalse: () => Effect.succeed(null), 977 - }, 978 - ), 979 - ), 980 - ), 981 - ), 929 + } 930 + ) 931 + ) 932 + ) 933 + ) 982 934 ); 983 935 984 936 // Retry fetching track until metadata is ready 985 937 const retryFetchTrack = ( 986 938 ctx: Context, 987 939 trackHash: string, 988 - initialTrack: SelectTrack | undefined, 940 + initialTrack: SelectTrack | undefined 989 941 ) => 990 942 pipe( 991 943 Effect.iterate( ··· 1001 953 .from(tables.tracks) 1002 954 .where(eq(tables.tracks.sha256, trackHash)) 1003 955 .execute() 1004 - .then(([row]) => row), 956 + .then(([row]) => row) 1005 957 ), 1006 958 Effect.flatMap((trackRecord) => 1007 959 Option.fromNullable(trackRecord).pipe( 1008 960 Effect.flatMap((track) => 1009 - updateTrackMetadata(ctx, track, trackRecord), 1010 - ), 1011 - ), 961 + updateTrackMetadata(ctx, track, trackRecord) 962 + ) 963 + ) 1012 964 ), 1013 965 Effect.tap((trackRecord) => 1014 966 Effect.logInfo( 1015 967 trackRecord 1016 968 ? `Track metadata ready: ${chalk.cyan(trackRecord.id)} - ${track.title}, after ${chalk.magenta(tries + 1)} tries` 1017 - : `Retrying track fetch: ${chalk.magenta(tries + 1)}`, 1018 - ), 969 + : `Retrying track fetch: ${chalk.magenta(tries + 1)}` 970 + ) 1019 971 ), 1020 972 Effect.map((trackRecord) => ({ 1021 973 tries: tries + 1, 1022 974 track: trackRecord, 1023 975 })), 1024 - Effect.delay("1 second"), 976 + Effect.delay("1 second") 1025 977 ), 1026 - }, 978 + } 1027 979 ), 1028 980 Effect.tap(({ tries, track }) => 1029 981 tries >= 30 && !(track?.artistUri && track?.albumUri) 1030 982 ? Effect.logError( 1031 - `Track metadata not ready after ${chalk.magenta("30 tries")}`, 983 + `Track metadata not ready after ${chalk.magenta("30 tries")}` 1032 984 ) 1033 - : Effect.succeed(undefined), 985 + : Effect.succeed(undefined) 1034 986 ), 1035 - Effect.map(({ track }) => track), 987 + Effect.map(({ track }) => track) 1036 988 ); 1037 989 1038 990 // Retry fetching scrobble until complete ··· 1070 1022 .from(tables.scrobbles) 1071 1023 .leftJoin( 1072 1024 tables.tracks, 1073 - eq(tables.scrobbles.trackId, tables.tracks.id), 1025 + eq(tables.scrobbles.trackId, tables.tracks.id) 1074 1026 ) 1075 1027 .leftJoin( 1076 1028 tables.albums, 1077 - eq(tables.scrobbles.albumId, tables.albums.id), 1029 + eq(tables.scrobbles.albumId, tables.albums.id) 1078 1030 ) 1079 1031 .leftJoin( 1080 1032 tables.artists, 1081 - eq(tables.scrobbles.artistId, tables.artists.id), 1033 + eq(tables.scrobbles.artistId, tables.artists.id) 1082 1034 ) 1083 1035 .leftJoin( 1084 1036 tables.users, 1085 - eq(tables.scrobbles.userId, tables.users.id), 1037 + eq(tables.scrobbles.userId, tables.users.id) 1086 1038 ) 1087 1039 .where(eq(tables.scrobbles.uri, scrobbleUri)) 1088 1040 .execute() 1089 - .then(([row]) => row), 1041 + .then(([row]) => row) 1090 1042 ), 1091 1043 Effect.tap((scrobble) => 1092 1044 Effect.if( ··· 1103 1055 artistUri: scrobble.artists.uri, 1104 1056 }) 1105 1057 .where(eq(tables.albums.id, scrobble.albums.id)) 1106 - .execute(), 1058 + .execute() 1107 1059 ), 1108 1060 onFalse: () => Effect.succeed(undefined), 1109 - }, 1110 - ), 1061 + } 1062 + ) 1111 1063 ), 1112 1064 Effect.flatMap(() => 1113 1065 Effect.tryPromise(() => ··· 1116 1068 .from(tables.scrobbles) 1117 1069 .leftJoin( 1118 1070 tables.tracks, 1119 - eq(tables.scrobbles.trackId, tables.tracks.id), 1071 + eq(tables.scrobbles.trackId, tables.tracks.id) 1120 1072 ) 1121 1073 .leftJoin( 1122 1074 tables.albums, 1123 - eq(tables.scrobbles.albumId, tables.albums.id), 1075 + eq(tables.scrobbles.albumId, tables.albums.id) 1124 1076 ) 1125 1077 .leftJoin( 1126 1078 tables.artists, 1127 - eq(tables.scrobbles.artistId, tables.artists.id), 1079 + eq(tables.scrobbles.artistId, tables.artists.id) 1128 1080 ) 1129 1081 .leftJoin( 1130 1082 tables.users, 1131 - eq(tables.scrobbles.userId, tables.users.id), 1083 + eq(tables.scrobbles.userId, tables.users.id) 1132 1084 ) 1133 1085 .where(eq(tables.scrobbles.uri, scrobbleUri)) 1134 1086 .execute() 1135 - .then(([row]) => row), 1136 - ), 1087 + .then(([row]) => row) 1088 + ) 1137 1089 ), 1138 1090 Effect.map((scrobble) => ({ 1139 1091 tries: tries + 1, ··· 1150 1102 scrobble.tracks.albumUri && 1151 1103 scrobble.scrobbles 1152 1104 ? `Scrobble found after ${chalk.magenta(tries + 1)} tries` 1153 - : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}`, 1154 - ), 1105 + : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}` 1106 + ) 1155 1107 ), 1156 - Effect.delay("1 second"), 1108 + Effect.delay("1 second") 1157 1109 ), 1158 - }, 1110 + } 1159 1111 ), 1160 1112 Effect.tap(({ tries, scrobble }) => 1161 1113 tries >= 30 && ··· 1169 1121 scrobble.tracks.albumUri 1170 1122 ) 1171 1123 ? Effect.logError( 1172 - `Scrobble not found after ${chalk.magenta("30 tries")}`, 1124 + `Scrobble not found after ${chalk.magenta("30 tries")}` 1173 1125 ) 1174 - : Effect.succeed(undefined), 1126 + : Effect.succeed(undefined) 1175 1127 ), 1176 - Effect.map(({ scrobble }) => scrobble), 1128 + Effect.map(({ scrobble }) => scrobble) 1177 1129 ); 1178 1130 1179 1131 export const scrobbleTrack = ( 1180 1132 ctx: Context, 1181 1133 track: Track, 1182 1134 agent: Agent, 1183 - userDid: string, 1135 + userDid: string 1184 1136 ) => 1185 1137 pipe( 1186 1138 computeTrackHash(track), ··· 1193 1145 Effect.flatMap(() => ensureAlbum(ctx, track, agent, userDid)), 1194 1146 Effect.flatMap(() => ensureArtist(ctx, track, agent, userDid)), 1195 1147 Effect.flatMap(() => 1196 - retryFetchTrack(ctx, trackHash, existingTrack), 1148 + retryFetchTrack(ctx, trackHash, existingTrack) 1197 1149 ), 1198 1150 Effect.flatMap(() => 1199 1151 pipe( ··· 1213 1165 ? pipe( 1214 1166 publishScrobble(ctx, scrobble.scrobbles.id), 1215 1167 Effect.tap(() => 1216 - Effect.logInfo("Scrobble published"), 1217 - ), 1168 + Effect.logInfo("Scrobble published") 1169 + ) 1218 1170 ) 1219 - : Effect.succeed(undefined), 1220 - ), 1221 - ), 1222 - ), 1223 - ), 1224 - ), 1225 - ), 1226 - ), 1227 - ), 1228 - ), 1171 + : Effect.succeed(undefined) 1172 + ) 1173 + ) 1174 + ) 1175 + ) 1176 + ) 1177 + ) 1178 + ) 1179 + ) 1180 + ) 1229 1181 );
+134 -175
apps/api/src/xrpc/app/rocksky/song/createSong.ts
··· 1 - import type { Agent, BlobRef } from "@atproto/api"; 1 + import type { Agent } from "@atproto/api"; 2 2 import { TID } from "@atproto/common"; 3 3 import type { HandlerAuth } from "@atproto/xrpc-server"; 4 4 import chalk from "chalk"; ··· 14 14 import type { SongViewDetailed } from "lexicon/types/app/rocksky/song/defs"; 15 15 import { deepSnakeCaseKeys } from "lib"; 16 16 import { createAgent } from "lib/agent"; 17 - import downloadImage from "lib/downloadImage"; 18 17 import { createHash } from "node:crypto"; 19 18 import tables from "schema"; 20 19 import type { InsertAlbumTrack, SelectAlbumTrack } from "schema/album-tracks"; ··· 44 43 Effect.catchAll((err) => { 45 44 console.error(err); 46 45 return Effect.succeed({}); 47 - }), 46 + }) 48 47 ); 49 48 server.app.rocksky.song.createSong({ 50 49 auth: ctx.authVerifier, ··· 78 77 ctx, 79 78 did, 80 79 input, 81 - })), 80 + })) 82 81 ), 83 82 Match.orElse(() => { 84 83 throw new Error("Authentication required to create a song"); 85 - }), 84 + }) 86 85 ), 87 86 catch: (error) => new Error(`Failed to create agent: ${error}`), 88 87 }); ··· 151 150 Effect.succeed( 152 151 createHash("sha256") 153 152 .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) 154 - .digest("hex"), 153 + .digest("hex") 155 154 ); 156 155 157 156 const computeAlbumHash = (track: Track): Effect.Effect<string, never> => 158 157 Effect.succeed( 159 158 createHash("sha256") 160 159 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 161 - .digest("hex"), 160 + .digest("hex") 162 161 ); 163 162 164 163 const computeArtistHash = (track: Track): Effect.Effect<string, never> => 165 164 Effect.succeed( 166 - createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex"), 165 + createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex") 167 166 ); 168 167 169 168 const fetchExistingTrack = ( 170 169 ctx: Context, 171 - trackHash: string, 170 + trackHash: string 172 171 ): Effect.Effect<SelectTrack | undefined, Error> => 173 172 Effect.tryPromise(() => 174 173 ctx.db ··· 176 175 .from(tables.tracks) 177 176 .where(eq(tables.tracks.sha256, trackHash)) 178 177 .execute() 179 - .then(([row]) => row), 180 - ); 181 - 182 - const uploadImage = (url: string, agent: Agent) => 183 - pipe( 184 - Effect.tryPromise(() => downloadImage(url)), 185 - Effect.map< 186 - Buffer<ArrayBufferLike>, 187 - [Buffer<ArrayBufferLike>, { encoding: string } | undefined] 188 - >((imageBuffer) => { 189 - if (url.endsWith(".jpeg") || url.endsWith(".jpg")) { 190 - return [imageBuffer, { encoding: "image/jpeg" }]; 191 - } else if (url.endsWith(".png")) { 192 - return [imageBuffer, { encoding: "image/png" }]; 193 - } 194 - return [imageBuffer, undefined]; 195 - }), 196 - Effect.flatMap(([imageBuffer, options]) => 197 - pipe( 198 - Effect.tryPromise(() => agent.uploadBlob(imageBuffer, options)), 199 - Effect.map((uploadResponse) => uploadResponse.data.blob), 200 - ), 201 - ), 202 - Effect.catchAll(() => Effect.succeed(undefined as BlobRef | undefined)), 178 + .then(([row]) => row) 203 179 ); 204 180 205 181 const generateRkey = Effect.succeed(TID.nextStr()); ··· 208 184 agent: Agent, 209 185 collection: string, 210 186 record: T, 211 - validate: (record: T) => { success: boolean }, 187 + validate: (record: T) => { success: boolean } 212 188 ): Effect.Effect<string, Error> => 213 189 pipe( 214 190 Effect.succeed(record), 215 191 Effect.filterOrFail( 216 192 (rec) => validate(rec).success, 217 - () => new Error("Invalid record"), 193 + () => new Error("Invalid record") 218 194 ), 219 195 Effect.flatMap(() => 220 196 pipe( ··· 227 203 rkey, 228 204 record, 229 205 validate: false, 230 - }), 231 - ), 206 + }) 207 + ) 232 208 ), 233 209 Effect.tap((res) => 234 - Effect.logInfo(`Record created at ${res.data.uri}`), 210 + Effect.logInfo(`Record created at ${res.data.uri}`) 235 211 ), 236 - Effect.map((res) => res.data.uri), 237 - ), 212 + Effect.map((res) => res.data.uri) 213 + ) 238 214 ), 239 215 Effect.catchAll((error) => { 240 216 console.error(`Error creating ${collection} record`, error); 241 217 return Effect.fail(error); 242 - }), 218 + }) 243 219 ); 244 220 245 221 const putArtistRecord = (track: Track, agent: Agent) => 246 222 pipe( 247 - track.artistPicture 248 - ? uploadImage(track.artistPicture, agent) 249 - : Effect.succeed(undefined), 250 - Effect.map((picture) => ({ 223 + Effect.succeed({ 251 224 $type: "app.rocksky.artist", 252 225 name: track.albumArtist, 253 226 createdAt: new Date().toISOString(), 254 - picture, 255 - })), 227 + pictureUrl: track.artistPicture, 228 + }), 256 229 Effect.flatMap((record) => 257 - putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord), 258 - ), 230 + putRecord(agent, "app.rocksky.artist", record, Artist.validateRecord) 231 + ) 259 232 ); 260 233 261 234 const putAlbumRecord = (track: Track, agent: Agent) => 262 235 pipe( 263 - Match.value(track.albumArt).pipe( 264 - Match.when( 265 - (url) => !!url, 266 - (url) => uploadImage(url, agent), 267 - ), 268 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 269 - ), 270 - Effect.map((albumArt) => ({ 236 + Effect.succeed({ 271 237 $type: "app.rocksky.album", 272 238 title: track.album, 273 239 artist: track.albumArtist, ··· 276 242 ? track.releaseDate.toISOString() 277 243 : undefined, 278 244 createdAt: new Date().toISOString(), 279 - albumArt, 280 - })), 245 + albumArtUrl: track.albumArt, 246 + }), 281 247 Effect.flatMap((record) => 282 - putRecord(agent, "app.rocksky.album", record, Album.validateRecord), 283 - ), 248 + putRecord(agent, "app.rocksky.album", record, Album.validateRecord) 249 + ) 284 250 ); 285 251 286 252 const putSongRecord = (track: Track, agent: Agent) => 287 253 pipe( 288 - Match.value(track.albumArt).pipe( 289 - Match.when( 290 - (url) => !!url, 291 - (url) => uploadImage(url, agent), 292 - ), 293 - Match.orElse(() => Effect.succeed(undefined as BlobRef | undefined)), 294 - ), 295 - Effect.map((albumArt) => ({ 254 + Effect.succeed({ 296 255 $type: "app.rocksky.song", 297 256 title: track.title, 298 257 artist: track.artist, ··· 303 262 ? track.releaseDate.toISOString() 304 263 : undefined, 305 264 year: track.year, 306 - albumArt, 265 + albumArtUrl: track.albumArt, 307 266 composer: track.composer ?? undefined, 308 267 lyrics: track.lyrics ?? undefined, 309 268 trackNumber: track.trackNumber, ··· 311 270 copyrightMessage: track.copyrightMessage ?? undefined, 312 271 createdAt: new Date().toISOString(), 313 272 spotifyLink: track.spotifyLink ?? undefined, 314 - })), 273 + }), 315 274 Effect.flatMap((record) => 316 - putRecord(agent, "app.rocksky.song", record, Song.validateRecord), 317 - ), 275 + putRecord(agent, "app.rocksky.song", record, Song.validateRecord) 276 + ) 318 277 ); 319 278 320 279 const ensureTrack = (ctx: Context, track: Track, agent: Agent) => ··· 329 288 Effect.tap((trackOpt) => 330 289 trackOpt 331 290 ? updateTrackMetadata(ctx, track, trackOpt) 332 - : Effect.succeed(undefined), 291 + : Effect.succeed(undefined) 333 292 ), 334 293 Effect.flatMap((trackOpt) => 335 294 trackOpt.uri 336 295 ? Effect.succeed(trackOpt.uri) 337 - : putSongRecord(track, agent), 338 - ), 339 - ), 340 - ), 341 - ), 342 - ), 296 + : putSongRecord(track, agent) 297 + ) 298 + ) 299 + ) 300 + ) 301 + ) 343 302 ); 344 303 345 304 // Update track metadata (album_uri and artist_uri) 346 305 const updateTrackMetadata = ( 347 306 ctx: Context, 348 307 track: Track, 349 - trackRecord: SelectTrack, 308 + trackRecord: SelectTrack 350 309 ) => 351 310 pipe( 352 311 Effect.succeed(trackRecord), ··· 361 320 .from(tables.albums) 362 321 .where(eq(tables.albums.sha256, albumHash)) 363 322 .execute() 364 - .then(([row]) => row), 365 - ), 323 + .then(([row]) => row) 324 + ) 366 325 ), 367 326 Effect.flatMap((album) => 368 327 Option.fromNullable(album).pipe( ··· 372 331 .update(tables.tracks) 373 332 .set({ albumUri: album.uri }) 374 333 .where(eq(tables.tracks.id, trackRecord.id)) 375 - .execute(), 376 - ), 334 + .execute() 335 + ) 377 336 ), 378 - Effect.catchAll(() => Effect.succeed(undefined)), 379 - ), 380 - ), 337 + Effect.catchAll(() => Effect.succeed(undefined)) 338 + ) 339 + ) 381 340 ) 382 - : Effect.succeed(undefined), 341 + : Effect.succeed(undefined) 383 342 ), 384 343 Effect.tap((trackRecord) => 385 344 !trackRecord.artistUri ··· 392 351 .from(tables.artists) 393 352 .where(eq(tables.artists.sha256, artistHash)) 394 353 .execute() 395 - .then(([row]) => row), 396 - ), 354 + .then(([row]) => row) 355 + ) 397 356 ), 398 357 Effect.flatMap((artist) => 399 358 Option.fromNullable(artist).pipe( ··· 403 362 .update(tables.tracks) 404 363 .set({ artistUri: artist.uri }) 405 364 .where(eq(tables.tracks.id, trackRecord.id)) 406 - .execute(), 407 - ), 365 + .execute() 366 + ) 408 367 ), 409 - Effect.catchAll(() => Effect.succeed(undefined)), 410 - ), 411 - ), 368 + Effect.catchAll(() => Effect.succeed(undefined)) 369 + ) 370 + ) 412 371 ) 413 - : Effect.succeed(undefined), 414 - ), 372 + : Effect.succeed(undefined) 373 + ) 415 374 ); 416 375 417 376 // Ensure artist exists or create it ··· 426 385 .from(tables.artists) 427 386 .where(eq(tables.artists.sha256, artistHash)) 428 387 .execute() 429 - .then(([row]) => row), 388 + .then(([row]) => row) 430 389 ), 431 390 Effect.flatMap((existingArtist) => 432 391 pipe( ··· 434 393 Effect.flatMap((artistOpt) => 435 394 artistOpt.uri 436 395 ? Effect.succeed(artistOpt.uri) 437 - : putArtistRecord(track, agent), 438 - ), 439 - ), 440 - ), 441 - ), 442 - ), 396 + : putArtistRecord(track, agent) 397 + ) 398 + ) 399 + ) 400 + ) 401 + ) 443 402 ); 444 403 445 404 // Ensure album exists or create it ··· 454 413 .from(tables.albums) 455 414 .where(eq(tables.albums.sha256, albumHash)) 456 415 .execute() 457 - .then(([row]) => row), 416 + .then(([row]) => row) 458 417 ), 459 418 Effect.flatMap((existingAlbum) => 460 419 pipe( ··· 462 421 Effect.flatMap((albumOpt) => 463 422 albumOpt.uri 464 423 ? Effect.succeed(albumOpt.uri) 465 - : putAlbumRecord(track, agent), 466 - ), 467 - ), 468 - ), 469 - ), 470 - ), 424 + : putAlbumRecord(track, agent) 425 + ) 426 + ) 427 + ) 428 + ) 429 + ) 471 430 ); 472 431 473 432 // Fetch track, album, and artist by URIs ··· 475 434 ctx: Context, 476 435 trackUri: string, 477 436 albumUri: string, 478 - artistUri: string, 437 + artistUri: string 479 438 ): Effect.Effect< 480 439 { 481 440 track: SelectTrack | null; ··· 491 450 .from(tables.tracks) 492 451 .where(eq(tables.tracks.uri, trackUri)) 493 452 .execute() 494 - .then(([row]) => row), 453 + .then(([row]) => row) 495 454 ), 496 455 album: Effect.tryPromise(() => 497 456 ctx.db ··· 499 458 .from(tables.albums) 500 459 .where(eq(tables.albums.uri, albumUri)) 501 460 .execute() 502 - .then(([row]) => row), 461 + .then(([row]) => row) 503 462 ), 504 463 artist: Effect.tryPromise(() => 505 464 ctx.db ··· 507 466 .from(tables.artists) 508 467 .where(eq(tables.artists.uri, artistUri)) 509 468 .execute() 510 - .then(([row]) => row), 469 + .then(([row]) => row) 511 470 ), 512 471 }); 513 472 ··· 516 475 ctx: Context, 517 476 track: SelectTrack, 518 477 album: SelectAlbum, 519 - artist: SelectArtist, 478 + artist: SelectArtist 520 479 ) => 521 480 pipe( 522 481 Effect.all({ ··· 527 486 .where( 528 487 and( 529 488 eq(tables.albumTracks.albumId, album.id), 530 - eq(tables.albumTracks.trackId, track.id), 531 - ), 489 + eq(tables.albumTracks.trackId, track.id) 490 + ) 532 491 ) 533 492 .execute() 534 - .then(([row]) => row), 493 + .then(([row]) => row) 535 494 ), 536 495 537 496 artistTrack: Effect.tryPromise(() => ··· 541 500 .where( 542 501 and( 543 502 eq(tables.artistTracks.artistId, artist.id), 544 - eq(tables.artistTracks.trackId, track.id), 545 - ), 503 + eq(tables.artistTracks.trackId, track.id) 504 + ) 546 505 ) 547 506 .execute() 548 - .then(([row]) => row), 507 + .then(([row]) => row) 549 508 ), 550 509 artistAlbum: Effect.tryPromise(() => 551 510 ctx.db ··· 554 513 .where( 555 514 and( 556 515 eq(tables.artistAlbums.artistId, artist.id), 557 - eq(tables.artistAlbums.albumId, album.id), 558 - ), 516 + eq(tables.artistAlbums.albumId, album.id) 517 + ) 559 518 ) 560 519 .execute() 561 - .then(([row]) => row), 520 + .then(([row]) => row) 562 521 ), 563 522 }), 564 523 Effect.flatMap(({ albumTrack, artistTrack, artistAlbum }) => ··· 576 535 } as InsertAlbumTrack) 577 536 .returning() 578 537 .execute() 579 - .then(([row]) => row), 580 - ), 581 - ), 538 + .then(([row]) => row) 539 + ) 540 + ) 582 541 ), 583 542 pipe( 584 543 Option.fromNullable(artistTrack), ··· 592 551 } as InsertArtistTrack) 593 552 .returning() 594 553 .execute() 595 - .then(([row]) => row), 596 - ), 597 - ), 554 + .then(([row]) => row) 555 + ) 556 + ) 598 557 ), 599 558 pipe( 600 559 Option.fromNullable(artistAlbum), ··· 608 567 } as InsertArtistAlbum) 609 568 .returning() 610 569 .execute() 611 - .then(([row]) => row), 612 - ), 613 - ), 570 + .then(([row]) => row) 571 + ) 572 + ) 614 573 ), 615 574 ]), 616 575 Effect.map(([albumTrack, artistTrack, artistAlbum]) => ({ 617 576 albumTrack, 618 577 artistTrack, 619 578 artistAlbum, 620 - })), 621 - ), 622 - ), 579 + })) 580 + ) 581 + ) 623 582 ); 624 583 625 584 // Update track with album and artist URIs if missing ··· 627 586 ctx: Context, 628 587 track: SelectTrack, 629 588 album: SelectAlbum, 630 - artist: SelectArtist, 589 + artist: SelectArtist 631 590 ) => 632 591 pipe( 633 592 Effect.succeed(track), ··· 640 599 albumUri: album.uri, 641 600 }) 642 601 .where(eq(tables.tracks.id, trackRecord.id)) 643 - .execute(), 602 + .execute() 644 603 ) 645 - : Effect.succeed(undefined), 604 + : Effect.succeed(undefined) 646 605 ), 647 606 Effect.tap((trackRecord) => 648 607 !trackRecord.artistUri ··· 653 612 artistUri: artist.uri, 654 613 }) 655 614 .where(eq(tables.tracks.id, trackRecord.id)) 656 - .execute(), 615 + .execute() 657 616 ) 658 - : Effect.succeed(undefined), 659 - ), 617 + : Effect.succeed(undefined) 618 + ) 660 619 ); 661 620 662 621 const publishTrack = ( ··· 664 623 track: SelectTrack, 665 624 albumTrack: SelectAlbumTrack, 666 625 artistTrack: SelectArtistTrack, 667 - artistAlbum: SelectArtistAlbum, 626 + artistAlbum: SelectArtistAlbum 668 627 ) => 669 628 pipe( 670 629 Effect.succeed( ··· 714 673 xata_updatedat: artistAlbum.updatedAt.toISOString(), 715 674 xata_version: artistAlbum.xataVersion, 716 675 }, 717 - }), 676 + }) 718 677 ), 719 678 Effect.flatMap((message) => 720 679 Effect.try(() => 721 680 ctx.nc.publish( 722 681 "rocksky.track", 723 - Buffer.from(JSON.stringify(message).replaceAll("sha_256", "sha256")), 724 - ), 725 - ), 726 - ), 682 + Buffer.from(JSON.stringify(message).replaceAll("sha_256", "sha256")) 683 + ) 684 + ) 685 + ) 727 686 ); 728 687 729 688 export const saveTrack = (ctx: Context, track: Track, agent: Agent) => ··· 776 735 Effect.filterOrFail( 777 736 () => !!track, 778 737 () => 779 - new Error(`Track not found for uri: ${trackUri}`), 780 - ), 738 + new Error(`Track not found for uri: ${trackUri}`) 739 + ) 781 740 ), 782 741 Option.fromNullable(album).pipe( 783 742 Effect.filterOrFail( 784 743 () => !!album, 785 744 () => 786 - new Error(`Album not found for uri: ${albumUri}`), 787 - ), 745 + new Error(`Album not found for uri: ${albumUri}`) 746 + ) 788 747 ), 789 748 Option.fromNullable(artist).pipe( 790 749 Effect.filterOrFail( 791 750 () => !!artist, 792 751 () => 793 - new Error(`Artist not found for uri: ${artistUri}`), 794 - ), 752 + new Error(`Artist not found for uri: ${artistUri}`) 753 + ) 795 754 ), 796 755 ]), 797 756 Effect.flatMap(([track, album, artist]) => 798 757 pipe( 799 758 updateTrackUris(ctx, track, album, artist), 800 759 Effect.flatMap(() => 801 - ensureRelationships(ctx, track, album, artist), 760 + ensureRelationships(ctx, track, album, artist) 802 761 ), 803 762 Effect.map( 804 763 ({ albumTrack, artistTrack, artistAlbum }) => ({ ··· 809 768 albumTrack, 810 769 artistTrack, 811 770 artistAlbum, 812 - }), 813 - ), 814 - ), 815 - ), 816 - ), 771 + }) 772 + ) 773 + ) 774 + ) 775 + ) 817 776 ), 818 777 Effect.tap( 819 778 ({ ··· 835 794 track.albumUri && 836 795 track.artistUri 837 796 ? `Track saved successfully after ${chalk.magenta(tries + 1)} tries` 838 - : `Track not yet saved, retrying... ${chalk.magenta(tries + 1)}`, 839 - ), 797 + : `Track not yet saved, retrying... ${chalk.magenta(tries + 1)}` 798 + ) 840 799 ), 841 800 Effect.tap( 842 801 ({ ··· 851 810 tries === 15 852 811 ? pipe( 853 812 Effect.logError( 854 - "Failed to save track after 15 tries", 813 + "Failed to save track after 15 tries" 855 814 ), 856 815 Effect.tap(() => 857 816 Effect.logDebug( 858 - `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)}`, 859 - ), 860 - ), 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 + ) 861 820 ) 862 - : Effect.succeed(undefined), 821 + : Effect.succeed(undefined) 863 822 ), 864 - Effect.delay("1 second"), 823 + Effect.delay("1 second") 865 824 ), 866 - }, 825 + } 867 826 ), 868 827 Effect.tap(({ tries, track, albumTrack, artistTrack, artistAlbum }) => 869 828 tries < 15 && track && albumTrack && artistTrack && artistAlbum 870 829 ? publishTrack(ctx, track, albumTrack, artistTrack, artistAlbum) 871 - : Effect.succeed(undefined), 872 - ), 873 - ), 874 - ), 830 + : Effect.succeed(undefined) 831 + ) 832 + ) 833 + ) 875 834 );