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

refactor createScrobble implementation

+622 -223
+16
rockskyapi/rocksky-auth/src/lib/index.ts
··· 40 40 } 41 41 return obj; 42 42 }; 43 + 44 + export const deepSnakeCaseKeys = <T>(obj: T): any => { 45 + if (Array.isArray(obj)) { 46 + return obj.map(deepSnakeCaseKeys); 47 + } else if (isObject(obj)) { 48 + return R.pipe( 49 + R.toPairs, 50 + R.map( 51 + ([key, value]) => 52 + [_.snakeCase(String(key)), deepSnakeCaseKeys(value)] as [string, any] 53 + ), 54 + R.fromPairs 55 + )(obj as object); 56 + } 57 + return obj; 58 + };
+4 -1
rockskyapi/rocksky-auth/src/schema/album-tracks.ts
··· 1 1 import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 - import { pgTable, text } from "drizzle-orm/pg-core"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 import albums from "./albums"; 4 4 import tracks from "./tracks"; 5 5 ··· 11 11 trackId: text("track_id") 12 12 .notNull() 13 13 .references(() => tracks.id), 14 + createdAt: timestamp("xata_createdat").defaultNow().notNull(), 15 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 16 + xataVersion: integer("xata_version").notNull(), 14 17 }); 15 18 16 19 export type SelectAlbumTrack = InferSelectModel<typeof albumTracks>;
+2
rockskyapi/rocksky-auth/src/schema/albums.ts
··· 16 16 youtubeLink: text("youtube_link").unique(), 17 17 sha256: text("sha256").unique().notNull(), 18 18 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 19 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 20 + xataVersion: integer("xata_version"), 19 21 }); 20 22 21 23 export type SelectAlbum = InferSelectModel<typeof albums>;
+22
rockskyapi/rocksky-auth/src/schema/artist-albums.ts
··· 1 + import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 + import albums from "./albums"; 4 + import artists from "./artists"; 5 + 6 + const artistAlbums = pgTable("artist_albums", { 7 + id: text("xata_id").primaryKey(), 8 + artistId: text("artist_id") 9 + .notNull() 10 + .references(() => artists.id), 11 + albumId: text("album_id") 12 + .notNull() 13 + .references(() => albums.id), 14 + createdAt: timestamp("xata_createdat").defaultNow().notNull(), 15 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 16 + xataVersion: integer("xata_version").notNull(), 17 + }); 18 + 19 + export type SelectArtistAlbum = InferSelectModel<typeof artistAlbums>; 20 + export type InsertArtistAlbum = InferInsertModel<typeof artistAlbums>; 21 + 22 + export default artistAlbums;
+22
rockskyapi/rocksky-auth/src/schema/artist-tracks.ts
··· 1 + import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 + import artists from "./artists"; 4 + import tracks from "./tracks"; 5 + 6 + const artistTracks = pgTable("artist_tracks", { 7 + id: text("xata_id").primaryKey(), 8 + artistId: text("artist_id") 9 + .notNull() 10 + .references(() => artists.id), 11 + trackId: text("track_id") 12 + .notNull() 13 + .references(() => tracks.id), 14 + createdAt: timestamp("xata_createdat").defaultNow().notNull(), 15 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 16 + xataVersion: integer("xata_version").notNull(), 17 + }); 18 + 19 + export type SelectArtistTrack = InferSelectModel<typeof artistTracks>; 20 + export type InsertArtistTrack = InferInsertModel<typeof artistTracks>; 21 + 22 + export default artistTracks;
+2 -1
rockskyapi/rocksky-auth/src/schema/artists.ts
··· 1 1 import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 - import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const artists = pgTable("artists", { 5 5 id: text("xata_id").primaryKey(), ··· 17 17 youtubeLink: text("youtube_link"), 18 18 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 19 19 updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 20 + xataVersion: integer("xata_version"), 20 21 }); 21 22 22 23 export type SelectArtist = InferSelectModel<typeof artists>;
+4
rockskyapi/rocksky-auth/src/schema/index.ts
··· 1 1 import albumTracks from "./album-tracks"; 2 2 import albums from "./albums"; 3 3 import apiKeys from "./api-keys"; 4 + import artistAlbums from "./artist-albums"; 5 + import artistTracks from "./artist-tracks"; 4 6 import artists from "./artists"; 5 7 import lovedTracks from "./loved-tracks"; 6 8 import playlistTracks from "./playlist-tracks"; ··· 42 44 lovedTracks, 43 45 spotifyAccounts, 44 46 spotifyTokens, 47 + artistTracks, 48 + artistAlbums, 45 49 };
+3 -1
rockskyapi/rocksky-auth/src/schema/scrobbles.ts
··· 1 - import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 1 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 2 3 3 import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 4 4 import albums from "./albums"; ··· 14 14 artistId: text("artist_id").references(() => artists.id), 15 15 uri: text("uri").unique(), 16 16 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 17 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 18 + xataVersion: integer("xata_version"), 17 19 timestamp: timestamp("timestamp").defaultNow().notNull(), 18 20 }); 19 21
+1
rockskyapi/rocksky-auth/src/schema/tracks.ts
··· 27 27 artistUri: text("artist_uri").unique(), 28 28 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 29 29 updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 30 + xataVersion: integer("xata_version"), 30 31 }); 31 32 32 33 export type SelectTrack = InferSelectModel<typeof tracks>;
+4 -1
rockskyapi/rocksky-auth/src/schema/user-albums.ts
··· 1 1 import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 - import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 import albums from "./albums"; 4 4 import users from "./users"; 5 5 ··· 12 12 .notNull() 13 13 .references(() => albums.id), 14 14 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 15 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 16 + xataVersion: integer("xata_version").notNull(), 17 + scrobbles: integer("scrobbles"), 15 18 uri: text("uri").unique().notNull(), 16 19 }); 17 20
+4 -1
rockskyapi/rocksky-auth/src/schema/user-artists.ts
··· 1 1 import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 - import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 import artists from "./artists"; 4 4 import users from "./users"; 5 5 ··· 12 12 .notNull() 13 13 .references(() => artists.id), 14 14 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 15 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 16 + xataVersion: integer("xata_version").notNull(), 17 + scrobbles: integer("scrobbles"), 15 18 uri: text("uri").unique().notNull(), 16 19 }); 17 20
+4 -1
rockskyapi/rocksky-auth/src/schema/user-tracks.ts
··· 1 1 import { InferInsertModel, InferSelectModel } from "drizzle-orm"; 2 - import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 import tracks from "./tracks"; 4 4 import users from "./users"; 5 5 ··· 12 12 .notNull() 13 13 .references(() => tracks.id), 14 14 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 15 + updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 16 + xataVersion: integer("xata_version").notNull(), 15 17 uri: text("uri").unique().notNull(), 18 + scrobbles: integer("scrobbles"), 16 19 }); 17 20 18 21 export type SelectUserTrack = InferSelectModel<typeof userTracks>;
+2 -1
rockskyapi/rocksky-auth/src/schema/users.ts
··· 1 1 import { InferSelectModel } from "drizzle-orm"; 2 - import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 + import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const users = pgTable("users", { 5 5 id: text("xata_id").primaryKey(), ··· 9 9 avatar: text("avatar").notNull(), 10 10 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 11 11 updatedAt: timestamp("xata_updatedat").defaultNow().notNull(), 12 + xataVersion: integer("xata_version"), 12 13 }); 13 14 14 15 export type SelectUser = InferSelectModel<typeof users>;
+532 -216
rockskyapi/rocksky-auth/src/xrpc/app/rocksky/scrobble/createScrobble.ts
··· 1 1 import { Agent, BlobRef } from "@atproto/api"; 2 2 import { TID } from "@atproto/common"; 3 3 import { HandlerAuth } from "@atproto/xrpc-server"; 4 - import { equals } from "@xata.io/client"; 5 4 import chalk from "chalk"; 6 5 import { Context } from "context"; 7 6 import dayjs from "dayjs"; 7 + import { and, eq } from "drizzle-orm"; 8 8 import { Effect, Match, Option, pipe } from "effect"; 9 9 import { Server } from "lexicon"; 10 10 import * as Album from "lexicon/types/app/rocksky/album"; ··· 13 13 import { InputSchema } from "lexicon/types/app/rocksky/scrobble/createScrobble"; 14 14 import { ScrobbleViewBasic } from "lexicon/types/app/rocksky/scrobble/defs"; 15 15 import * as Song from "lexicon/types/app/rocksky/song"; 16 + import { deepSnakeCaseKeys } from "lib"; 16 17 import { createAgent } from "lib/agent"; 17 18 import downloadImage from "lib/downloadImage"; 18 19 import { createHash } from "node:crypto"; 20 + import tables from "schema"; 21 + import { SelectAlbum } from "schema/albums"; 22 + import { SelectArtist } from "schema/artists"; 23 + import { SelectTrack } from "schema/tracks"; 24 + import { InsertUserAlbum } from "schema/user-albums"; 25 + import { InsertUserArtist } from "schema/user-artists"; 26 + import { InsertUserTrack } from "schema/user-tracks"; 27 + import { SelectUser } from "schema/users"; 19 28 import { Track, trackSchema } from "types/track"; 20 29 21 30 export default function (server: Server, ctx: Context) { ··· 283 292 ) 284 293 ); 285 294 286 - const publishScrobble = (ctx: Context, id: string) => 295 + const getScrobbles = ({ ctx, id }: { ctx: Context; id: string }) => 296 + Effect.tryPromise(() => 297 + ctx.db 298 + .select() 299 + .from(tables.scrobbles) 300 + .leftJoin(tables.tracks, eq(tables.tracks.id, tables.scrobbles.trackId)) 301 + .leftJoin(tables.albums, eq(tables.albums.id, tables.scrobbles.albumId)) 302 + .leftJoin( 303 + tables.artists, 304 + eq(tables.artists.id, tables.scrobbles.artistId) 305 + ) 306 + .leftJoin(tables.users, eq(tables.users.id, tables.scrobbles.userId)) 307 + .where(eq(tables.scrobbles.id, id)) 308 + .execute() 309 + .then(([row]) => row) 310 + ); 311 + 312 + const getUserAlbum = ( 313 + ctx: Context, 314 + scrobble: { 315 + albums: SelectAlbum; 316 + artists: SelectArtist; 317 + users: SelectUser; 318 + tracks: SelectTrack; 319 + } 320 + ) => 321 + Effect.tryPromise(() => 322 + ctx.db 323 + .select() 324 + .from(tables.userAlbums) 325 + .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 326 + .execute() 327 + .then(([row]) => row) 328 + ); 329 + 330 + const getUserArtist = ( 331 + ctx: Context, 332 + scrobble: { 333 + albums: SelectAlbum; 334 + artists: SelectArtist; 335 + users: SelectUser; 336 + tracks: SelectTrack; 337 + } 338 + ) => 339 + Effect.tryPromise(() => 340 + ctx.db 341 + .select() 342 + .from(tables.userArtists) 343 + .where(eq(tables.userArtists.id, scrobble.artists.id)) 344 + .execute() 345 + .then(([row]) => row) 346 + ); 347 + 348 + const getUserTrack = ( 349 + ctx: Context, 350 + scrobble: { 351 + albums: SelectAlbum; 352 + artists: SelectArtist; 353 + users: SelectUser; 354 + tracks: SelectTrack; 355 + } 356 + ) => 357 + Effect.tryPromise(() => 358 + ctx.db 359 + .select() 360 + .from(tables.userTracks) 361 + .where(eq(tables.userTracks.id, scrobble.tracks.id)) 362 + .execute() 363 + .then(([row]) => row) 364 + ); 365 + 366 + const getAlbumTrack = ( 367 + ctx: Context, 368 + scrobble: { 369 + albums: SelectAlbum; 370 + artists: SelectArtist; 371 + users: SelectUser; 372 + tracks: SelectTrack; 373 + } 374 + ) => 375 + Effect.tryPromise(() => 376 + ctx.db 377 + .select() 378 + .from(tables.albumTracks) 379 + .where(eq(tables.albumTracks.trackId, scrobble.tracks.id)) 380 + .execute() 381 + .then(([row]) => row) 382 + ); 383 + 384 + const getArtistTrack = ( 385 + ctx: Context, 386 + scrobble: { 387 + albums: SelectAlbum; 388 + artists: SelectArtist; 389 + users: SelectUser; 390 + tracks: SelectTrack; 391 + } 392 + ) => 393 + Effect.tryPromise(() => 394 + ctx.db 395 + .select() 396 + .from(tables.artistTracks) 397 + .where(eq(tables.artistTracks.trackId, scrobble.tracks.id)) 398 + .execute() 399 + .then(([row]) => row) 400 + ); 401 + 402 + const getArtistAlbum = ( 403 + ctx: Context, 404 + scrobble: { 405 + albums: SelectAlbum; 406 + artists: SelectArtist; 407 + users: SelectUser; 408 + tracks: SelectTrack; 409 + } 410 + ) => 411 + Effect.tryPromise(() => 412 + ctx.db 413 + .select() 414 + .from(tables.artistAlbums) 415 + .where( 416 + and( 417 + eq(tables.artistAlbums.albumId, scrobble.albums.id), 418 + eq(tables.artistAlbums.artistId, scrobble.artists.id) 419 + ) 420 + ) 421 + .then(([row]) => row) 422 + ); 423 + 424 + const createUserArtist = ( 425 + ctx: Context, 426 + scrobble: { 427 + albums: SelectAlbum; 428 + artists: SelectArtist; 429 + users: SelectUser; 430 + tracks: SelectTrack; 431 + } 432 + ) => 287 433 pipe( 288 434 Effect.tryPromise(() => 289 - ctx.client.db.scrobbles 290 - .select(["*", "track_id.*", "album_id.*", "artist_id.*", "user_id.*"]) 291 - .filter("xata_id", equals(id)) 292 - .getFirst() 435 + ctx.db 436 + .insert(tables.userArtists) 437 + .values({ 438 + userId: scrobble.users.id, 439 + artistId: scrobble.artists.id, 440 + uri: scrobble.artists.uri, 441 + scrobbles: 1, 442 + } as InsertUserArtist) 443 + .execute() 293 444 ), 445 + Effect.flatMap(() => 446 + Effect.tryPromise(() => 447 + ctx.db 448 + .select() 449 + .from(tables.userArtists) 450 + .where(eq(tables.userArtists.artistId, scrobble.artists.id)) 451 + .execute() 452 + .then(([row]) => row) 453 + ) 454 + ) 455 + ); 456 + 457 + const createUserAlbum = ( 458 + ctx: Context, 459 + scrobble: { 460 + albums: SelectAlbum; 461 + artists: SelectArtist; 462 + users: SelectUser; 463 + tracks: SelectTrack; 464 + } 465 + ) => 466 + pipe( 467 + Effect.tryPromise(() => 468 + ctx.db 469 + .insert(tables.userAlbums) 470 + .values({ 471 + userId: scrobble.users.id, 472 + albumId: scrobble.albums.id, 473 + uri: scrobble.albums.uri, 474 + scrobbles: 1, 475 + } as InsertUserAlbum) 476 + .execute() 477 + ), 478 + Effect.flatMap(() => 479 + Effect.tryPromise(() => 480 + ctx.db 481 + .select() 482 + .from(tables.userAlbums) 483 + .where(eq(tables.userAlbums.albumId, scrobble.albums.id)) 484 + .execute() 485 + .then(([row]) => row) 486 + ) 487 + ) 488 + ); 489 + 490 + const createUserTrack = ( 491 + ctx: Context, 492 + scrobble: { 493 + albums: SelectAlbum; 494 + artists: SelectArtist; 495 + users: SelectUser; 496 + tracks: SelectTrack; 497 + } 498 + ) => 499 + pipe( 500 + Effect.tryPromise(() => 501 + ctx.db 502 + .insert(tables.userTracks) 503 + .values({ 504 + userId: scrobble.users.id, 505 + trackId: scrobble.tracks.id, 506 + uri: scrobble.tracks.uri, 507 + scrobbles: 1, 508 + } as InsertUserTrack) 509 + .execute() 510 + ), 511 + Effect.flatMap(() => 512 + Effect.tryPromise(() => 513 + ctx.db 514 + .select() 515 + .from(tables.userTracks) 516 + .where(eq(tables.userTracks.trackId, scrobble.tracks.id)) 517 + .then(([row]) => row) 518 + ) 519 + ) 520 + ); 521 + 522 + const publishScrobble = (ctx: Context, id: string) => 523 + pipe( 524 + { ctx, id }, 525 + getScrobbles, 294 526 Effect.flatMap((scrobble) => 295 527 pipe( 296 528 Effect.all([ 297 - Effect.tryPromise(() => 298 - ctx.client.db.user_albums 299 - .select(["*"]) 300 - .filter("album_id.xata_id", equals(scrobble.album_id.xata_id)) 301 - .getFirst() 302 - ), 303 - Effect.tryPromise(() => 304 - ctx.client.db.user_artists 305 - .select(["*"]) 306 - .filter("artist_id.xata_id", equals(scrobble.artist_id.xata_id)) 307 - .getFirst() 308 - ), 309 - Effect.tryPromise(() => 310 - ctx.client.db.user_tracks 311 - .select(["*"]) 312 - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) 313 - .getFirst() 314 - ), 315 - Effect.tryPromise(() => 316 - ctx.client.db.album_tracks 317 - .select(["*"]) 318 - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) 319 - .getFirst() 320 - ), 321 - Effect.tryPromise(() => 322 - ctx.client.db.artist_tracks 323 - .select(["*"]) 324 - .filter("track_id.xata_id", equals(scrobble.track_id.xata_id)) 325 - .getFirst() 326 - ), 327 - Effect.tryPromise(() => 328 - ctx.client.db.artist_albums 329 - .select(["*"]) 330 - .filter("album_id.xata_id", equals(scrobble.album_id.xata_id)) 331 - .filter("artist_id.xata_id", equals(scrobble.artist_id.xata_id)) 332 - .getFirst() 333 - ), 529 + getUserAlbum(ctx, scrobble), 530 + getUserArtist(ctx, scrobble), 531 + getUserTrack(ctx, scrobble), 532 + getAlbumTrack(ctx, scrobble), 533 + getArtistTrack(ctx, scrobble), 534 + getArtistAlbum(ctx, scrobble), 334 535 ]), 335 536 Effect.flatMap( 336 537 ([ ··· 343 544 ]) => 344 545 pipe( 345 546 Option.fromNullable(userArtist), 346 - Effect.orElse(() => 347 - pipe( 348 - Effect.tryPromise(() => 349 - ctx.client.db.user_artists.create({ 350 - user_id: scrobble.user_id.xata_id, 351 - artist_id: scrobble.artist_id.xata_id, 352 - uri: scrobble.artist_id.uri, 353 - scrobbles: 1, 354 - }) 355 - ), 356 - Effect.flatMap(() => 357 - Effect.tryPromise(() => 358 - ctx.client.db.user_artists 359 - .select(["*"]) 360 - .filter( 361 - "artist_id.xata_id", 362 - equals(scrobble.artist_id.xata_id) 363 - ) 364 - .getFirst() 365 - ) 366 - ) 367 - ) 368 - ), 547 + Effect.orElse(() => createUserArtist(ctx, scrobble)), 369 548 Effect.flatMap((finalUserArtist) => 370 549 pipe( 371 550 Option.fromNullable(userAlbum), 372 - Effect.orElse(() => 373 - pipe( 374 - Effect.tryPromise(() => 375 - ctx.client.db.user_albums.create({ 376 - user_id: scrobble.user_id.xata_id, 377 - album_id: scrobble.album_id.xata_id, 378 - uri: scrobble.album_id.uri, 379 - scrobbles: 1, 380 - }) 381 - ), 382 - Effect.flatMap(() => 383 - Effect.tryPromise(() => 384 - ctx.client.db.user_albums 385 - .select(["*"]) 386 - .filter( 387 - "album_id.xata_id", 388 - equals(scrobble.album_id.xata_id) 389 - ) 390 - .getFirst() 391 - ) 392 - ) 393 - ) 394 - ), 551 + Effect.orElse(() => createUserAlbum(ctx, scrobble)), 395 552 Effect.flatMap((finalUserAlbum) => 396 553 pipe( 397 554 Option.fromNullable(userTrack), 398 - Effect.orElse(() => 399 - pipe( 400 - Effect.tryPromise(() => 401 - ctx.client.db.user_tracks.create({ 402 - user_id: scrobble.user_id.xata_id, 403 - track_id: scrobble.track_id.xata_id, 404 - uri: scrobble.track_id.uri, 405 - scrobbles: 1, 406 - }) 407 - ), 408 - Effect.flatMap(() => 409 - Effect.tryPromise(() => 410 - ctx.client.db.user_tracks 411 - .select(["*"]) 412 - .filter( 413 - "track_id.xata_id", 414 - equals(scrobble.track_id.xata_id) 415 - ) 416 - .getFirst() 417 - ) 418 - ) 419 - ) 420 - ), 421 - Effect.map((finalUserTrack) => ({ 422 - scrobble, 423 - user_album: finalUserAlbum, 424 - user_artist: finalUserArtist, 425 - user_track: finalUserTrack, 426 - album_track: albumTrack, 427 - artist_track: artistTrack, 428 - artist_album: artistAlbum, 429 - })) 555 + Effect.orElse(() => createUserTrack(ctx, scrobble)), 556 + Effect.map((finalUserTrack) => 557 + deepSnakeCaseKeys({ 558 + scrobble: { 559 + album_id: { 560 + ...scrobble.albums, 561 + xata_createdat: scrobble.albums.createdAt, 562 + xata_id: scrobble.albums.id, 563 + xata_updatedat: scrobble.albums.updatedAt, 564 + xata_version: scrobble.albums.xataVersion, 565 + }, 566 + artist_id: { 567 + ...scrobble.artists, 568 + xata_createdat: scrobble.artists.createdAt, 569 + xata_id: scrobble.artists.id, 570 + xata_updatedat: scrobble.artists.updatedAt, 571 + xata_version: scrobble.artists.xataVersion, 572 + }, 573 + track_id: { 574 + ...scrobble.tracks, 575 + xata_createdat: scrobble.tracks.createdAt, 576 + xata_id: scrobble.tracks.id, 577 + xata_updatedat: scrobble.tracks.updatedAt, 578 + xata_version: scrobble.tracks.xataVersion, 579 + }, 580 + uri: scrobble.scrobbles.uri, 581 + user_id: { 582 + avatar: scrobble.users.avatar, 583 + did: scrobble.users.did, 584 + display_name: scrobble.users.displayName, 585 + handle: scrobble.users.handle, 586 + xata_createdat: scrobble.users.createdAt, 587 + xata_id: scrobble.users.id, 588 + xata_updatedat: scrobble.users.updatedAt, 589 + xata_version: scrobble.users.xataVersion, 590 + }, 591 + xata_createdat: scrobble.scrobbles.createdAt, 592 + xata_id: scrobble.scrobbles.id, 593 + xata_updatedat: scrobble.scrobbles.updatedAt, 594 + xata_version: scrobble.scrobbles.xataVersion, 595 + }, 596 + user_album: { 597 + album_id: { xata_id: finalUserAlbum.albumId }, 598 + scrobbles: finalUserAlbum.scrobbles, 599 + uri: finalUserAlbum.uri, 600 + user_id: { 601 + xata_id: finalUserAlbum.userId, 602 + }, 603 + xata_createdat: finalUserAlbum.createdAt, 604 + xata_id: finalUserAlbum.id, 605 + xata_updatedat: finalUserAlbum.updatedAt, 606 + xata_version: finalUserAlbum.xataVersion, 607 + }, 608 + user_artist: { 609 + artist_id: { 610 + xata_id: finalUserArtist.artistId, 611 + }, 612 + scrobbles: finalUserArtist.scrobbles, 613 + uri: finalUserArtist.uri, 614 + user_id: { 615 + xata_id: finalUserArtist.userId, 616 + }, 617 + xata_createdat: finalUserArtist.createdAt, 618 + xata_id: finalUserArtist.id, 619 + xata_updatedat: finalUserArtist.updatedAt, 620 + xata_version: finalUserArtist.xataVersion, 621 + }, 622 + user_track: { 623 + scrobbles: finalUserTrack.scrobbles, 624 + track_id: { 625 + xata_id: finalUserTrack.trackId, 626 + }, 627 + uri: finalUserTrack.uri, 628 + user_id: { 629 + xata_id: finalUserTrack.userId, 630 + }, 631 + xata_createdat: finalUserTrack.createdAt, 632 + xata_id: finalUserTrack.id, 633 + xata_updatedat: finalUserTrack.updatedAt, 634 + xata_version: finalUserTrack.xataVersion, 635 + }, 636 + album_track: albumTrack, 637 + artist_track: artistTrack, 638 + artist_album: artistAlbum, 639 + }) 640 + ) 430 641 ) 431 642 ) 432 643 ) ··· 464 675 createHash("sha256").update(track.albumArtist.toLowerCase()).digest("hex") 465 676 ); 466 677 467 - const fetchExistingTrack = (ctx: Context, trackHash: string) => 678 + const fetchExistingTrack = ( 679 + ctx: Context, 680 + trackHash: string 681 + ): Effect.Effect<SelectTrack | undefined, Error> => 468 682 Effect.tryPromise(() => 469 - ctx.client.db.tracks.filter("sha256", equals(trackHash)).getFirst() 683 + ctx.db 684 + .select() 685 + .from(tables.tracks) 686 + .where(eq(tables.tracks.sha256, trackHash)) 687 + .execute() 688 + .then(([row]) => row) 470 689 ); 471 690 472 691 // Update track metadata (album_uri and artist_uri) 473 - const updateTrackMetadata = (ctx: Context, track: Track, trackRecord: any) => 692 + const updateTrackMetadata = ( 693 + ctx: Context, 694 + track: Track, 695 + trackRecord: SelectTrack 696 + ) => 474 697 pipe( 475 698 Effect.succeed(trackRecord), 476 699 Effect.tap((trackRecord) => 477 - !trackRecord.album_uri 700 + !trackRecord.albumUri 478 701 ? pipe( 479 702 computeAlbumHash(track), 480 703 Effect.flatMap((albumHash) => 481 704 Effect.tryPromise(() => 482 - ctx.client.db.albums 483 - .filter("sha256", equals(albumHash)) 484 - .getFirst() 705 + ctx.db 706 + .select() 707 + .from(tables.albums) 708 + .where(eq(tables.albums.sha256, albumHash)) 709 + .execute() 710 + .then(([row]) => row) 485 711 ) 486 712 ), 487 713 Effect.flatMap((album) => 488 714 album 489 715 ? Effect.tryPromise(() => 490 - ctx.client.db.tracks.update(trackRecord.xata_id, { 491 - album_uri: album.uri, 492 - }) 716 + ctx.db 717 + .update(tables.tracks) 718 + .set({ 719 + albumUri: album.uri, 720 + }) 721 + .where(eq(tables.tracks.id, trackRecord.id)) 722 + .execute() 493 723 ) 494 724 : Effect.succeed(undefined) 495 725 ) ··· 497 727 : Effect.succeed(undefined) 498 728 ), 499 729 Effect.tap((trackRecord) => 500 - !trackRecord.artist_uri 730 + !trackRecord.artistUri 501 731 ? pipe( 502 732 computeArtistHash(track), 503 733 Effect.flatMap((artistHash) => 504 734 Effect.tryPromise(() => 505 - ctx.client.db.artists 506 - .filter("sha256", equals(artistHash)) 507 - .getFirst() 735 + ctx.db 736 + .select() 737 + .from(tables.artists) 738 + .where(eq(tables.artists.sha256, artistHash)) 739 + .execute() 740 + .then(([row]) => row) 508 741 ) 509 742 ), 510 743 Effect.flatMap((artist) => 511 744 artist 512 745 ? Effect.tryPromise(() => 513 - ctx.client.db.tracks.update(trackRecord.xata_id, { 514 - artist_uri: artist.uri, 515 - }) 746 + ctx.db 747 + .update(tables.tracks) 748 + .set({ 749 + artistUri: artist.uri, 750 + }) 751 + .where(eq(tables.tracks.id, trackRecord.id)) 752 + .execute() 516 753 ) 517 754 : Effect.succeed(undefined) 518 755 ) ··· 527 764 track: Track, 528 765 agent: Agent, 529 766 userDid: string, 530 - existingTrack: any 767 + existingTrack: SelectTrack | undefined 531 768 ) => 532 769 pipe( 533 - Option.fromNullable(existingTrack), 770 + Effect.succeed(existingTrack), 534 771 Effect.tap((trackOpt) => 535 772 Match.value(trackOpt).pipe( 536 773 Match.when( 537 - (value) => Option.isSome(value), 538 - () => updateTrackMetadata(ctx, track, trackOpt.value) 774 + (value) => !!value, 775 + () => updateTrackMetadata(ctx, track, trackOpt) 539 776 ), 540 777 Match.orElse(() => Effect.succeed(undefined)) 541 778 ) ··· 543 780 Effect.flatMap((trackOpt) => 544 781 pipe( 545 782 Effect.tryPromise(() => 546 - ctx.client.db.user_tracks 547 - .filter({ 548 - "track_id.xata_id": trackOpt?.xata_id, 549 - "user_id.did": userDid, 550 - }) 551 - .getFirst() 783 + ctx.db 784 + .select() 785 + .from(tables.userTracks) 786 + .leftJoin( 787 + tables.tracks, 788 + eq(tables.userTracks.trackId, tables.tracks.id) 789 + ) 790 + .leftJoin( 791 + tables.users, 792 + eq(tables.userTracks.userId, tables.users.id) 793 + ) 794 + .where( 795 + and( 796 + eq(tables.tracks.id, trackOpt?.id), 797 + eq(tables.users.did, userDid) 798 + ) 799 + ) 800 + .execute() 801 + .then(([row]) => row.user_tracks) 552 802 ), 553 803 Effect.flatMap((userTrack) => 554 804 Option.isNone(Option.fromNullable(userTrack)) || ··· 571 821 computeAlbumHash(track), 572 822 Effect.flatMap((albumHash) => 573 823 Effect.tryPromise(() => 574 - ctx.client.db.albums.filter("sha256", equals(albumHash)).getFirst() 824 + ctx.db 825 + .select() 826 + .from(tables.albums) 827 + .where(eq(tables.albums.sha256, albumHash)) 828 + .execute() 829 + .then(([row]) => row) 575 830 ) 576 831 ), 577 832 Effect.flatMap((existingAlbum) => ··· 579 834 Option.fromNullable(existingAlbum), 580 835 Effect.flatMap((album) => 581 836 Effect.tryPromise(() => 582 - ctx.client.db.user_albums 583 - .filter({ 584 - "album_id.xata_id": album.xata_id, 585 - "user_id.did": userDid, 586 - }) 587 - .getFirst() 837 + ctx.db 838 + .select() 839 + .from(tables.userAlbums) 840 + .leftJoin( 841 + tables.albums, 842 + eq(tables.userAlbums.albumId, tables.albums.id) 843 + ) 844 + .leftJoin( 845 + tables.users, 846 + eq(tables.userAlbums.userId, tables.users.id) 847 + ) 848 + .where( 849 + and( 850 + eq(tables.albums.id, album.id), 851 + eq(tables.users.did, userDid) 852 + ) 853 + ) 854 + .execute() 855 + .then(([row]) => row.user_albums) 588 856 ) 589 857 ), 590 858 Effect.flatMap((userAlbum) => ··· 609 877 computeArtistHash(track), 610 878 Effect.flatMap((artistHash) => 611 879 Effect.tryPromise(() => 612 - ctx.client.db.artists.filter("sha256", equals(artistHash)).getFirst() 880 + ctx.db 881 + .select() 882 + .from(tables.artists) 883 + .where(eq(tables.artists.sha256, artistHash)) 884 + .execute() 885 + .then(([row]) => row) 613 886 ) 614 887 ), 615 888 Effect.flatMap((existingArtist) => ··· 617 890 Option.fromNullable(existingArtist), 618 891 Effect.flatMap((artist) => 619 892 Effect.tryPromise(() => 620 - ctx.client.db.user_artists 621 - .filter({ 622 - "artist_id.xata_id": artist.xata_id, 623 - "user_id.did": userDid, 624 - }) 625 - .getFirst() 893 + ctx.db 894 + .select() 895 + .from(tables.userArtists) 896 + .leftJoin( 897 + tables.artists, 898 + eq(tables.userArtists.artistId, tables.artists.id) 899 + ) 900 + .leftJoin( 901 + tables.users, 902 + eq(tables.userArtists.userId, tables.users.id) 903 + ) 904 + .where( 905 + and( 906 + eq(tables.artists.id, artist.id), 907 + eq(tables.users.did, userDid) 908 + ) 909 + ) 910 + .execute() 911 + .then(([row]) => row.user_artists) 626 912 ) 627 913 ), 628 914 Effect.flatMap((userArtist) => ··· 643 929 // Retry fetching track until metadata is ready 644 930 const retryFetchTrack = ( 645 931 ctx: Context, 646 - track: Track, 647 932 trackHash: string, 648 - initialTrack: any 933 + initialTrack: SelectTrack | undefined 649 934 ) => 650 935 pipe( 651 936 Effect.iterate( 652 937 { tries: 0, track: initialTrack }, 653 938 { 654 939 while: ({ tries, track }) => 655 - tries < 30 && !(track?.artist_uri && track?.album_uri), 940 + tries < 30 && !(track?.artistUri && track?.albumUri), 656 941 body: ({ tries, track }) => 657 942 pipe( 658 943 Effect.tryPromise(() => 659 - ctx.client.db.tracks 660 - .filter("sha256", equals(trackHash)) 661 - .getFirst() 944 + ctx.db 945 + .select() 946 + .from(tables.tracks) 947 + .where(eq(tables.tracks.sha256, trackHash)) 948 + .execute() 949 + .then(([row]) => row) 662 950 ), 663 951 Effect.flatMap((trackRecord) => 664 952 Option.fromNullable(trackRecord).pipe( ··· 670 958 Effect.tap((trackRecord) => 671 959 Effect.logInfo( 672 960 trackRecord 673 - ? `Track metadata ready: ${chalk.cyan(trackRecord.xata_id)} - ${track.title}, after ${chalk.magenta(tries + 1)} tries` 961 + ? `Track metadata ready: ${chalk.cyan(trackRecord.id)} - ${track.title}, after ${chalk.magenta(tries + 1)} tries` 674 962 : `Retrying track fetch: ${chalk.magenta(tries + 1)}` 675 963 ) 676 964 ), ··· 683 971 } 684 972 ), 685 973 Effect.tap(({ tries, track }) => 686 - tries >= 30 && !(track?.artist_uri && track?.album_uri) 974 + tries >= 30 && !(track?.artistUri && track?.albumUri) 687 975 ? Effect.logError( 688 976 `Track metadata not ready after ${chalk.magenta("30 tries")}` 689 977 ) ··· 712 1000 body: ({ tries }) => 713 1001 pipe( 714 1002 Effect.tryPromise(() => 715 - ctx.client.db.scrobbles 716 - .select([ 717 - "*", 718 - "track_id.*", 719 - "album_id.*", 720 - "artist_id.*", 721 - "user_id.*", 722 - ]) 723 - .filter("uri", equals(scrobbleUri)) 724 - .getFirst() 1003 + ctx.db 1004 + .select() 1005 + .from(tables.scrobbles) 1006 + .leftJoin( 1007 + tables.tracks, 1008 + eq(tables.scrobbles.trackId, tables.tracks.id) 1009 + ) 1010 + .leftJoin( 1011 + tables.albums, 1012 + eq(tables.scrobbles.albumId, tables.albums.id) 1013 + ) 1014 + .leftJoin( 1015 + tables.artists, 1016 + eq(tables.scrobbles.artistId, tables.artists.id) 1017 + ) 1018 + .leftJoin( 1019 + tables.users, 1020 + eq(tables.scrobbles.userId, tables.users.id) 1021 + ) 1022 + .where(eq(tables.scrobbles.uri, scrobbleUri)) 1023 + .execute() 1024 + .then(([row]) => row) 725 1025 ), 726 1026 Effect.tap((scrobble) => 727 1027 Effect.if( 728 1028 !!scrobble && 729 - !!scrobble.album_id && 730 - !scrobble.album_id.artist_uri && 731 - !!scrobble.artist_id.uri, 1029 + !!scrobble.albums && 1030 + !scrobble.albums.artistUri && 1031 + !!scrobble.artists.uri, 732 1032 { 733 1033 onTrue: () => 734 1034 Effect.tryPromise(() => 735 - ctx.client.db.albums.update(scrobble.album_id.xata_id, { 736 - artist_uri: scrobble.artist_id.uri, 737 - }) 1035 + ctx.db 1036 + .update(tables.albums) 1037 + .set({ 1038 + artistUri: scrobble.artists.uri, 1039 + }) 1040 + .where(eq(tables.albums.id, scrobble.albums.id)) 1041 + .execute() 738 1042 ), 739 1043 onFalse: () => Effect.succeed(undefined), 740 1044 } ··· 742 1046 ), 743 1047 Effect.flatMap(() => 744 1048 Effect.tryPromise(() => 745 - ctx.client.db.scrobbles 746 - .select([ 747 - "*", 748 - "track_id.*", 749 - "album_id.*", 750 - "artist_id.*", 751 - "user_id.*", 752 - ]) 753 - .filter("uri", equals(scrobbleUri)) 754 - .getFirst() 1049 + ctx.db 1050 + .select() 1051 + .from(tables.scrobbles) 1052 + .leftJoin( 1053 + tables.tracks, 1054 + eq(tables.scrobbles.trackId, tables.tracks.id) 1055 + ) 1056 + .leftJoin( 1057 + tables.albums, 1058 + eq(tables.scrobbles.albumId, tables.albums.id) 1059 + ) 1060 + .leftJoin( 1061 + tables.artists, 1062 + eq(tables.scrobbles.artistId, tables.artists.id) 1063 + ) 1064 + .leftJoin( 1065 + tables.users, 1066 + eq(tables.scrobbles.userId, tables.users.id) 1067 + ) 1068 + .where(eq(tables.scrobbles.uri, scrobbleUri)) 1069 + .execute() 1070 + .then(([row]) => row) 755 1071 ) 756 1072 ), 757 1073 Effect.map((scrobble) => ({ ··· 761 1077 Effect.tap(({ scrobble, tries }) => 762 1078 Effect.logInfo( 763 1079 scrobble && 764 - scrobble.track_id && 765 - scrobble.album_id && 766 - scrobble.artist_id && 767 - scrobble.album_id.artist_uri && 768 - scrobble.track_id.artist_uri && 769 - scrobble.track_id.album_uri 1080 + scrobble.tracks && 1081 + scrobble.albums && 1082 + scrobble.artists && 1083 + scrobble.albums.artistUri && 1084 + scrobble.tracks.artistUri && 1085 + scrobble.tracks.albumUri 770 1086 ? `Scrobble found after ${chalk.magenta(tries + 1)} tries` 771 1087 : `Scrobble not found, trying again: ${chalk.magenta(tries + 1)}` 772 1088 ) ··· 779 1095 tries >= 30 && 780 1096 !( 781 1097 scrobble && 782 - scrobble.track_id && 783 - scrobble.album_id && 784 - scrobble.artist_id && 785 - scrobble.album_id.artist_uri && 786 - scrobble.track_id.artist_uri && 787 - scrobble.track_id.album_uri 1098 + scrobble.tracks && 1099 + scrobble.albums && 1100 + scrobble.artists && 1101 + scrobble.albums.artistUri && 1102 + scrobble.tracks.artistUri && 1103 + scrobble.tracks.albumUri 788 1104 ) 789 1105 ? Effect.logError( 790 1106 `Scrobble not found after ${chalk.magenta("30 tries")}` ··· 811 1127 Effect.flatMap(() => ensureAlbum(ctx, track, agent, userDid)), 812 1128 Effect.flatMap(() => ensureArtist(ctx, track, agent, userDid)), 813 1129 Effect.flatMap(() => 814 - retryFetchTrack(ctx, track, trackHash, existingTrack) 1130 + retryFetchTrack(ctx, trackHash, existingTrack) 815 1131 ), 816 1132 Effect.flatMap(() => 817 1133 pipe( ··· 821 1137 retryFetchScrobble(ctx, scrobbleUri), 822 1138 Effect.flatMap((scrobble) => 823 1139 scrobble && 824 - scrobble.track_id && 825 - scrobble.album_id && 826 - scrobble.artist_id && 827 - scrobble.album_id.artist_uri && 828 - scrobble.track_id.artist_uri && 829 - scrobble.track_id.album_uri 1140 + scrobble.tracks && 1141 + scrobble.albums && 1142 + scrobble.artists && 1143 + scrobble.albums.artistUri && 1144 + scrobble.tracks.artistUri && 1145 + scrobble.tracks.albumUri 830 1146 ? pipe( 831 - publishScrobble(ctx, scrobble.xata_id), 1147 + publishScrobble(ctx, scrobble.id), 832 1148 Effect.tap(() => 833 1149 Effect.logInfo("Scrobble published") 834 1150 )