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

Add names filter to artist getArtists

Introduce an optional "names" parameter and propagate it through
lexicon, pkl, TypeScript types, XRPC handler, and analytics types. Also
apply assorted formatting, trailing-comma, and import fixes.

+434 -361
+4
apps/api/lexicons/artist/getArtists.json
··· 17 "type": "integer", 18 "description": "The offset for pagination", 19 "minimum": 0 20 } 21 } 22 },
··· 17 "type": "integer", 18 "description": "The offset for pagination", 19 "minimum": 0 20 + }, 21 + "names": { 22 + "type": "string", 23 + "description": "The names of the artists to return" 24 } 25 } 26 },
+4
apps/api/pkl/defs/artist/getArtists.pkl
··· 18 description = "The offset for pagination" 19 minimum = 0 20 } 21 } 22 } 23 output {
··· 18 description = "The offset for pagination" 19 minimum = 0 20 } 21 + ["names"] = new StringType { 22 + type = "string" 23 + description = "The names of the artists to return" 24 + } 25 } 26 } 27 output {
+1 -1
apps/api/src/auth/client.ts
··· 29 client_id: publicUrl 30 ? `${url}/oauth-client-metadata.json` 31 : `http://localhost?redirect_uri=${enc( 32 - `${url}/oauth/callback` 33 )}&scope=${enc("atproto transition:generic")}`, 34 client_uri: url, 35 redirect_uris: [`${url}/oauth/callback`],
··· 29 client_id: publicUrl 30 ? `${url}/oauth-client-metadata.json` 31 : `http://localhost?redirect_uri=${enc( 32 + `${url}/oauth/callback`, 33 )}&scope=${enc("atproto transition:generic")}`, 34 client_uri: url, 35 redirect_uris: [`${url}/oauth/callback`],
+1 -1
apps/api/src/auth/storage.ts
··· 47 .insertInto("auth_session") 48 .values({ key, session, expiresAt: val.tokenSet.expires_at }) 49 .onConflict((oc) => 50 - oc.doUpdateSet({ session, expiresAt: val.tokenSet.expires_at }) 51 ) 52 .execute(); 53 }
··· 47 .insertInto("auth_session") 48 .values({ key, session, expiresAt: val.tokenSet.expires_at }) 49 .onConflict((oc) => 50 + oc.doUpdateSet({ session, expiresAt: val.tokenSet.expires_at }), 51 ) 52 .execute(); 53 }
+12 -9
apps/api/src/bsky/app.ts
··· 81 ? Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 1000 82 : Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, 83 }, 84 - env.JWT_SECRET 85 ); 86 ctx.kv.set(did, token); 87 } catch (err) { ··· 93 .select() 94 .from(spotifyAccounts) 95 .where( 96 - and(eq(spotifyAccounts.userId, did), eq(spotifyAccounts.isBetaUser, true)) 97 ) 98 .limit(1) 99 .execute(); ··· 179 180 ctx.nc.publish( 181 "rocksky.user", 182 - Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))) 183 ); 184 185 await ctx.kv.set("lastUser", lastUser[0].id); ··· 192 .where( 193 and( 194 eq(spotifyAccounts.userId, did), 195 - eq(spotifyAccounts.isBetaUser, true) 196 - ) 197 ) 198 .limit(1) 199 .execute(), ··· 209 .where( 210 and( 211 eq(googleDriveAccounts.userId, did), 212 - eq(googleDriveAccounts.isBetaUser, true) 213 - ) 214 ) 215 .limit(1) 216 .execute(), ··· 220 .where( 221 and( 222 eq(dropboxAccounts.userId, did), 223 - eq(dropboxAccounts.isBetaUser, true) 224 - ) 225 ) 226 .limit(1) 227 .execute(),
··· 81 ? Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 1000 82 : Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, 83 }, 84 + env.JWT_SECRET, 85 ); 86 ctx.kv.set(did, token); 87 } catch (err) { ··· 93 .select() 94 .from(spotifyAccounts) 95 .where( 96 + and( 97 + eq(spotifyAccounts.userId, did), 98 + eq(spotifyAccounts.isBetaUser, true), 99 + ), 100 ) 101 .limit(1) 102 .execute(); ··· 182 183 ctx.nc.publish( 184 "rocksky.user", 185 + Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))), 186 ); 187 188 await ctx.kv.set("lastUser", lastUser[0].id); ··· 195 .where( 196 and( 197 eq(spotifyAccounts.userId, did), 198 + eq(spotifyAccounts.isBetaUser, true), 199 + ), 200 ) 201 .limit(1) 202 .execute(), ··· 212 .where( 213 and( 214 eq(googleDriveAccounts.userId, did), 215 + eq(googleDriveAccounts.isBetaUser, true), 216 + ), 217 ) 218 .limit(1) 219 .execute(), ··· 223 .where( 224 and( 225 eq(dropboxAccounts.userId, did), 226 + eq(dropboxAccounts.isBetaUser, true), 227 + ), 228 ) 229 .limit(1) 230 .execute(),
+3 -3
apps/api/src/db.ts
··· 131 132 export const refreshSessionsAboutToExpire = async ( 133 db: Database, 134 - ctx: Context 135 ) => { 136 const now = new Date().toISOString(); 137 ··· 147 console.log( 148 "Session about to expire:", 149 chalk.cyan(session.key), 150 - session.expiresAt 151 ); 152 const agent = await createAgent(ctx.oauthClient, session.key); 153 // Trigger a token refresh by fetching preferences ··· 156 } 157 158 console.log( 159 - `Found ${chalk.yellowBright(sessions.length)} sessions to refresh` 160 ); 161 }; 162
··· 131 132 export const refreshSessionsAboutToExpire = async ( 133 db: Database, 134 + ctx: Context, 135 ) => { 136 const now = new Date().toISOString(); 137 ··· 147 console.log( 148 "Session about to expire:", 149 chalk.cyan(session.key), 150 + session.expiresAt, 151 ); 152 const agent = await createAgent(ctx.oauthClient, session.key); 153 // Trigger a token refresh by fetching preferences ··· 156 } 157 158 console.log( 159 + `Found ${chalk.yellowBright(sessions.length)} sessions to refresh`, 160 ); 161 }; 162
+3 -3
apps/api/src/index.ts
··· 50 rateLimiter({ 51 limit: 1000, 52 window: 30, // 👈 30 seconds 53 - }) 54 ); 55 56 app.use("*", async (c, next) => { ··· 165 ctx.redis.get(`nowplaying:${user.did}:status`), 166 ]); 167 return c.json( 168 - nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {} 169 ); 170 }); 171 ··· 317 listeners: 1, 318 sha256: item.track.sha256, 319 id: item.scrobble.id, 320 - })) 321 ); 322 }); 323
··· 50 rateLimiter({ 51 limit: 1000, 52 window: 30, // 👈 30 seconds 53 + }), 54 ); 55 56 app.use("*", async (c, next) => { ··· 165 ctx.redis.get(`nowplaying:${user.did}:status`), 166 ]); 167 return c.json( 168 + nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {}, 169 ); 170 }); 171 ··· 317 listeners: 1, 318 sha256: item.track.sha256, 319 id: item.scrobble.id, 320 + })), 321 ); 322 }); 323
+39 -39
apps/api/src/lexicon/index.ts
··· 22 import type * as AppRockskyActorGetActorSongs from "./types/app/rocksky/actor/getActorSongs"; 23 import type * as AppRockskyActorGetProfile from "./types/app/rocksky/actor/getProfile"; 24 import type * as AppRockskyAlbumGetAlbum from "./types/app/rocksky/album/getAlbum"; 25 - import type * as AppRockskyAlbumGetAlbums from "./types/app/rocksky/album/getAlbums"; 26 import type * as AppRockskyAlbumGetAlbumTracks from "./types/app/rocksky/album/getAlbumTracks"; 27 import type * as AppRockskyApikeyCreateApikey from "./types/app/rocksky/apikey/createApikey"; 28 import type * as AppRockskyApikeyGetApikeys from "./types/app/rocksky/apikey/getApikeys"; 29 import type * as AppRockskyApikeyRemoveApikey from "./types/app/rocksky/apikey/removeApikey"; 30 import type * as AppRockskyApikeyUpdateApikey from "./types/app/rocksky/apikey/updateApikey"; 31 - import type * as AppRockskyArtistGetArtistAlbums from "./types/app/rocksky/artist/getArtistAlbums"; 32 import type * as AppRockskyArtistGetArtist from "./types/app/rocksky/artist/getArtist"; 33 import type * as AppRockskyArtistGetArtistListeners from "./types/app/rocksky/artist/getArtistListeners"; 34 import type * as AppRockskyArtistGetArtists from "./types/app/rocksky/artist/getArtists"; 35 - import type * as AppRockskyArtistGetArtistTracks from "./types/app/rocksky/artist/getArtistTracks"; 36 import type * as AppRockskyChartsGetScrobblesChart from "./types/app/rocksky/charts/getScrobblesChart"; 37 import type * as AppRockskyDropboxDownloadFile from "./types/app/rocksky/dropbox/downloadFile"; 38 import type * as AppRockskyDropboxGetFiles from "./types/app/rocksky/dropbox/getFiles"; ··· 53 import type * as AppRockskyPlayerGetPlaybackQueue from "./types/app/rocksky/player/getPlaybackQueue"; 54 import type * as AppRockskyPlayerNext from "./types/app/rocksky/player/next"; 55 import type * as AppRockskyPlayerPause from "./types/app/rocksky/player/pause"; 56 import type * as AppRockskyPlayerPlayDirectory from "./types/app/rocksky/player/playDirectory"; 57 import type * as AppRockskyPlayerPlayFile from "./types/app/rocksky/player/playFile"; 58 - import type * as AppRockskyPlayerPlay from "./types/app/rocksky/player/play"; 59 import type * as AppRockskyPlayerPrevious from "./types/app/rocksky/player/previous"; 60 import type * as AppRockskyPlayerSeek from "./types/app/rocksky/player/seek"; 61 import type * as AppRockskyPlaylistCreatePlaylist from "./types/app/rocksky/playlist/createPlaylist"; ··· 365 return this._server.xrpc.method(nsid, cfg); 366 } 367 368 - getAlbums<AV extends AuthVerifier>( 369 cfg: ConfigOf< 370 AV, 371 - AppRockskyAlbumGetAlbums.Handler<ExtractAuth<AV>>, 372 - AppRockskyAlbumGetAlbums.HandlerReqCtx<ExtractAuth<AV>> 373 >, 374 ) { 375 - const nsid = "app.rocksky.album.getAlbums"; // @ts-ignore 376 return this._server.xrpc.method(nsid, cfg); 377 } 378 379 - getAlbumTracks<AV extends AuthVerifier>( 380 cfg: ConfigOf< 381 AV, 382 - AppRockskyAlbumGetAlbumTracks.Handler<ExtractAuth<AV>>, 383 - AppRockskyAlbumGetAlbumTracks.HandlerReqCtx<ExtractAuth<AV>> 384 >, 385 ) { 386 - const nsid = "app.rocksky.album.getAlbumTracks"; // @ts-ignore 387 return this._server.xrpc.method(nsid, cfg); 388 } 389 } ··· 447 this._server = server; 448 } 449 450 - getArtistAlbums<AV extends AuthVerifier>( 451 cfg: ConfigOf< 452 AV, 453 - AppRockskyArtistGetArtistAlbums.Handler<ExtractAuth<AV>>, 454 - AppRockskyArtistGetArtistAlbums.HandlerReqCtx<ExtractAuth<AV>> 455 >, 456 ) { 457 - const nsid = "app.rocksky.artist.getArtistAlbums"; // @ts-ignore 458 return this._server.xrpc.method(nsid, cfg); 459 } 460 461 - getArtist<AV extends AuthVerifier>( 462 cfg: ConfigOf< 463 AV, 464 - AppRockskyArtistGetArtist.Handler<ExtractAuth<AV>>, 465 - AppRockskyArtistGetArtist.HandlerReqCtx<ExtractAuth<AV>> 466 >, 467 ) { 468 - const nsid = "app.rocksky.artist.getArtist"; // @ts-ignore 469 return this._server.xrpc.method(nsid, cfg); 470 } 471 ··· 480 return this._server.xrpc.method(nsid, cfg); 481 } 482 483 - getArtists<AV extends AuthVerifier>( 484 cfg: ConfigOf< 485 AV, 486 - AppRockskyArtistGetArtists.Handler<ExtractAuth<AV>>, 487 - AppRockskyArtistGetArtists.HandlerReqCtx<ExtractAuth<AV>> 488 >, 489 ) { 490 - const nsid = "app.rocksky.artist.getArtists"; // @ts-ignore 491 return this._server.xrpc.method(nsid, cfg); 492 } 493 494 - getArtistTracks<AV extends AuthVerifier>( 495 cfg: ConfigOf< 496 AV, 497 - AppRockskyArtistGetArtistTracks.Handler<ExtractAuth<AV>>, 498 - AppRockskyArtistGetArtistTracks.HandlerReqCtx<ExtractAuth<AV>> 499 >, 500 ) { 501 - const nsid = "app.rocksky.artist.getArtistTracks"; // @ts-ignore 502 return this._server.xrpc.method(nsid, cfg); 503 } 504 } ··· 770 return this._server.xrpc.method(nsid, cfg); 771 } 772 773 playDirectory<AV extends AuthVerifier>( 774 cfg: ConfigOf< 775 AV, ··· 789 >, 790 ) { 791 const nsid = "app.rocksky.player.playFile"; // @ts-ignore 792 - return this._server.xrpc.method(nsid, cfg); 793 - } 794 - 795 - play<AV extends AuthVerifier>( 796 - cfg: ConfigOf< 797 - AV, 798 - AppRockskyPlayerPlay.Handler<ExtractAuth<AV>>, 799 - AppRockskyPlayerPlay.HandlerReqCtx<ExtractAuth<AV>> 800 - >, 801 - ) { 802 - const nsid = "app.rocksky.player.play"; // @ts-ignore 803 return this._server.xrpc.method(nsid, cfg); 804 } 805
··· 22 import type * as AppRockskyActorGetActorSongs from "./types/app/rocksky/actor/getActorSongs"; 23 import type * as AppRockskyActorGetProfile from "./types/app/rocksky/actor/getProfile"; 24 import type * as AppRockskyAlbumGetAlbum from "./types/app/rocksky/album/getAlbum"; 25 import type * as AppRockskyAlbumGetAlbumTracks from "./types/app/rocksky/album/getAlbumTracks"; 26 + import type * as AppRockskyAlbumGetAlbums from "./types/app/rocksky/album/getAlbums"; 27 import type * as AppRockskyApikeyCreateApikey from "./types/app/rocksky/apikey/createApikey"; 28 import type * as AppRockskyApikeyGetApikeys from "./types/app/rocksky/apikey/getApikeys"; 29 import type * as AppRockskyApikeyRemoveApikey from "./types/app/rocksky/apikey/removeApikey"; 30 import type * as AppRockskyApikeyUpdateApikey from "./types/app/rocksky/apikey/updateApikey"; 31 import type * as AppRockskyArtistGetArtist from "./types/app/rocksky/artist/getArtist"; 32 + import type * as AppRockskyArtistGetArtistAlbums from "./types/app/rocksky/artist/getArtistAlbums"; 33 import type * as AppRockskyArtistGetArtistListeners from "./types/app/rocksky/artist/getArtistListeners"; 34 + import type * as AppRockskyArtistGetArtistTracks from "./types/app/rocksky/artist/getArtistTracks"; 35 import type * as AppRockskyArtistGetArtists from "./types/app/rocksky/artist/getArtists"; 36 import type * as AppRockskyChartsGetScrobblesChart from "./types/app/rocksky/charts/getScrobblesChart"; 37 import type * as AppRockskyDropboxDownloadFile from "./types/app/rocksky/dropbox/downloadFile"; 38 import type * as AppRockskyDropboxGetFiles from "./types/app/rocksky/dropbox/getFiles"; ··· 53 import type * as AppRockskyPlayerGetPlaybackQueue from "./types/app/rocksky/player/getPlaybackQueue"; 54 import type * as AppRockskyPlayerNext from "./types/app/rocksky/player/next"; 55 import type * as AppRockskyPlayerPause from "./types/app/rocksky/player/pause"; 56 + import type * as AppRockskyPlayerPlay from "./types/app/rocksky/player/play"; 57 import type * as AppRockskyPlayerPlayDirectory from "./types/app/rocksky/player/playDirectory"; 58 import type * as AppRockskyPlayerPlayFile from "./types/app/rocksky/player/playFile"; 59 import type * as AppRockskyPlayerPrevious from "./types/app/rocksky/player/previous"; 60 import type * as AppRockskyPlayerSeek from "./types/app/rocksky/player/seek"; 61 import type * as AppRockskyPlaylistCreatePlaylist from "./types/app/rocksky/playlist/createPlaylist"; ··· 365 return this._server.xrpc.method(nsid, cfg); 366 } 367 368 + getAlbumTracks<AV extends AuthVerifier>( 369 cfg: ConfigOf< 370 AV, 371 + AppRockskyAlbumGetAlbumTracks.Handler<ExtractAuth<AV>>, 372 + AppRockskyAlbumGetAlbumTracks.HandlerReqCtx<ExtractAuth<AV>> 373 >, 374 ) { 375 + const nsid = "app.rocksky.album.getAlbumTracks"; // @ts-ignore 376 return this._server.xrpc.method(nsid, cfg); 377 } 378 379 + getAlbums<AV extends AuthVerifier>( 380 cfg: ConfigOf< 381 AV, 382 + AppRockskyAlbumGetAlbums.Handler<ExtractAuth<AV>>, 383 + AppRockskyAlbumGetAlbums.HandlerReqCtx<ExtractAuth<AV>> 384 >, 385 ) { 386 + const nsid = "app.rocksky.album.getAlbums"; // @ts-ignore 387 return this._server.xrpc.method(nsid, cfg); 388 } 389 } ··· 447 this._server = server; 448 } 449 450 + getArtist<AV extends AuthVerifier>( 451 cfg: ConfigOf< 452 AV, 453 + AppRockskyArtistGetArtist.Handler<ExtractAuth<AV>>, 454 + AppRockskyArtistGetArtist.HandlerReqCtx<ExtractAuth<AV>> 455 >, 456 ) { 457 + const nsid = "app.rocksky.artist.getArtist"; // @ts-ignore 458 return this._server.xrpc.method(nsid, cfg); 459 } 460 461 + getArtistAlbums<AV extends AuthVerifier>( 462 cfg: ConfigOf< 463 AV, 464 + AppRockskyArtistGetArtistAlbums.Handler<ExtractAuth<AV>>, 465 + AppRockskyArtistGetArtistAlbums.HandlerReqCtx<ExtractAuth<AV>> 466 >, 467 ) { 468 + const nsid = "app.rocksky.artist.getArtistAlbums"; // @ts-ignore 469 return this._server.xrpc.method(nsid, cfg); 470 } 471 ··· 480 return this._server.xrpc.method(nsid, cfg); 481 } 482 483 + getArtistTracks<AV extends AuthVerifier>( 484 cfg: ConfigOf< 485 AV, 486 + AppRockskyArtistGetArtistTracks.Handler<ExtractAuth<AV>>, 487 + AppRockskyArtistGetArtistTracks.HandlerReqCtx<ExtractAuth<AV>> 488 >, 489 ) { 490 + const nsid = "app.rocksky.artist.getArtistTracks"; // @ts-ignore 491 return this._server.xrpc.method(nsid, cfg); 492 } 493 494 + getArtists<AV extends AuthVerifier>( 495 cfg: ConfigOf< 496 AV, 497 + AppRockskyArtistGetArtists.Handler<ExtractAuth<AV>>, 498 + AppRockskyArtistGetArtists.HandlerReqCtx<ExtractAuth<AV>> 499 >, 500 ) { 501 + const nsid = "app.rocksky.artist.getArtists"; // @ts-ignore 502 return this._server.xrpc.method(nsid, cfg); 503 } 504 } ··· 770 return this._server.xrpc.method(nsid, cfg); 771 } 772 773 + play<AV extends AuthVerifier>( 774 + cfg: ConfigOf< 775 + AV, 776 + AppRockskyPlayerPlay.Handler<ExtractAuth<AV>>, 777 + AppRockskyPlayerPlay.HandlerReqCtx<ExtractAuth<AV>> 778 + >, 779 + ) { 780 + const nsid = "app.rocksky.player.play"; // @ts-ignore 781 + return this._server.xrpc.method(nsid, cfg); 782 + } 783 + 784 playDirectory<AV extends AuthVerifier>( 785 cfg: ConfigOf< 786 AV, ··· 800 >, 801 ) { 802 const nsid = "app.rocksky.player.playFile"; // @ts-ignore 803 return this._server.xrpc.method(nsid, cfg); 804 } 805
+95 -91
apps/api/src/lexicon/lexicons.ts
··· 1267 }, 1268 }, 1269 }, 1270 - AppRockskyAlbumGetAlbums: { 1271 lexicon: 1, 1272 - id: "app.rocksky.album.getAlbums", 1273 defs: { 1274 main: { 1275 type: "query", 1276 - description: "Get albums", 1277 parameters: { 1278 type: "params", 1279 properties: { 1280 - limit: { 1281 - type: "integer", 1282 - description: "The maximum number of albums to return", 1283 - minimum: 1, 1284 - }, 1285 - offset: { 1286 - type: "integer", 1287 - description: "The offset for pagination", 1288 - minimum: 0, 1289 }, 1290 }, 1291 }, ··· 1294 schema: { 1295 type: "object", 1296 properties: { 1297 - albums: { 1298 type: "array", 1299 items: { 1300 type: "ref", 1301 - ref: "lex:app.rocksky.album.defs#albumViewBasic", 1302 }, 1303 }, 1304 }, ··· 1307 }, 1308 }, 1309 }, 1310 - AppRockskyAlbumGetAlbumTracks: { 1311 lexicon: 1, 1312 - id: "app.rocksky.album.getAlbumTracks", 1313 defs: { 1314 main: { 1315 type: "query", 1316 - description: "Get tracks for an album", 1317 parameters: { 1318 type: "params", 1319 - required: ["uri"], 1320 properties: { 1321 - uri: { 1322 - type: "string", 1323 - description: "The URI of the album to retrieve tracks from", 1324 - format: "at-uri", 1325 }, 1326 }, 1327 }, ··· 1330 schema: { 1331 type: "object", 1332 properties: { 1333 - tracks: { 1334 type: "array", 1335 items: { 1336 type: "ref", 1337 - ref: "lex:app.rocksky.song.defs#songViewBasic", 1338 }, 1339 }, 1340 }, ··· 1737 }, 1738 }, 1739 }, 1740 AppRockskyArtistGetArtistAlbums: { 1741 lexicon: 1, 1742 id: "app.rocksky.artist.getArtistAlbums", ··· 1768 }, 1769 }, 1770 }, 1771 - }, 1772 - }, 1773 - }, 1774 - }, 1775 - }, 1776 - AppRockskyArtistGetArtist: { 1777 - lexicon: 1, 1778 - id: "app.rocksky.artist.getArtist", 1779 - defs: { 1780 - main: { 1781 - type: "query", 1782 - description: "Get artist details", 1783 - parameters: { 1784 - type: "params", 1785 - required: ["uri"], 1786 - properties: { 1787 - uri: { 1788 - type: "string", 1789 - description: "The URI of the artist to retrieve details from", 1790 - format: "at-uri", 1791 - }, 1792 - }, 1793 - }, 1794 - output: { 1795 - encoding: "application/json", 1796 - schema: { 1797 - type: "ref", 1798 - ref: "lex:app.rocksky.artist.defs#artistViewDetailed", 1799 }, 1800 }, 1801 }, ··· 1845 }, 1846 }, 1847 }, 1848 - AppRockskyArtistGetArtists: { 1849 lexicon: 1, 1850 - id: "app.rocksky.artist.getArtists", 1851 defs: { 1852 main: { 1853 type: "query", 1854 - description: "Get artists", 1855 parameters: { 1856 type: "params", 1857 properties: { 1858 limit: { 1859 type: "integer", 1860 - description: "The maximum number of artists to return", 1861 minimum: 1, 1862 }, 1863 offset: { ··· 1872 schema: { 1873 type: "object", 1874 properties: { 1875 - artists: { 1876 type: "array", 1877 items: { 1878 type: "ref", 1879 - ref: "lex:app.rocksky.artist.defs#artistViewBasic", 1880 }, 1881 }, 1882 }, ··· 1885 }, 1886 }, 1887 }, 1888 - AppRockskyArtistGetArtistTracks: { 1889 lexicon: 1, 1890 - id: "app.rocksky.artist.getArtistTracks", 1891 defs: { 1892 main: { 1893 type: "query", 1894 - description: "Get artist's tracks", 1895 parameters: { 1896 type: "params", 1897 properties: { 1898 - uri: { 1899 - type: "string", 1900 - description: "The URI of the artist to retrieve albums from", 1901 - format: "at-uri", 1902 - }, 1903 limit: { 1904 type: "integer", 1905 - description: "The maximum number of tracks to return", 1906 minimum: 1, 1907 }, 1908 offset: { 1909 type: "integer", 1910 description: "The offset for pagination", 1911 minimum: 0, 1912 }, 1913 }, 1914 }, ··· 1917 schema: { 1918 type: "object", 1919 properties: { 1920 - tracks: { 1921 type: "array", 1922 items: { 1923 type: "ref", 1924 - ref: "lex:app.rocksky.song.defs#songViewBasic", 1925 }, 1926 }, 1927 }, ··· 2766 }, 2767 }, 2768 }, 2769 AppRockskyPlayerPlayDirectory: { 2770 lexicon: 1, 2771 id: "app.rocksky.player.playDirectory", ··· 2812 type: "string", 2813 }, 2814 fileId: { 2815 - type: "string", 2816 - }, 2817 - }, 2818 - }, 2819 - }, 2820 - }, 2821 - }, 2822 - AppRockskyPlayerPlay: { 2823 - lexicon: 1, 2824 - id: "app.rocksky.player.play", 2825 - defs: { 2826 - main: { 2827 - type: "procedure", 2828 - description: "Resume playback of the currently paused track", 2829 - parameters: { 2830 - type: "params", 2831 - properties: { 2832 - playerId: { 2833 type: "string", 2834 }, 2835 }, ··· 5058 AppRockskyAlbum: "app.rocksky.album", 5059 AppRockskyAlbumDefs: "app.rocksky.album.defs", 5060 AppRockskyAlbumGetAlbum: "app.rocksky.album.getAlbum", 5061 - AppRockskyAlbumGetAlbums: "app.rocksky.album.getAlbums", 5062 AppRockskyAlbumGetAlbumTracks: "app.rocksky.album.getAlbumTracks", 5063 AppRockskyApikeyCreateApikey: "app.rocksky.apikey.createApikey", 5064 AppRockskyApikeyDefs: "app.rocksky.apikey.defs", 5065 AppRockskyApikeysDefs: "app.rocksky.apikeys.defs", ··· 5068 AppRockskyApikeyUpdateApikey: "app.rocksky.apikey.updateApikey", 5069 AppRockskyArtist: "app.rocksky.artist", 5070 AppRockskyArtistDefs: "app.rocksky.artist.defs", 5071 - AppRockskyArtistGetArtistAlbums: "app.rocksky.artist.getArtistAlbums", 5072 AppRockskyArtistGetArtist: "app.rocksky.artist.getArtist", 5073 AppRockskyArtistGetArtistListeners: "app.rocksky.artist.getArtistListeners", 5074 - AppRockskyArtistGetArtists: "app.rocksky.artist.getArtists", 5075 AppRockskyArtistGetArtistTracks: "app.rocksky.artist.getArtistTracks", 5076 AppRockskyChartsDefs: "app.rocksky.charts.defs", 5077 AppRockskyChartsGetScrobblesChart: "app.rocksky.charts.getScrobblesChart", 5078 AppRockskyDropboxDefs: "app.rocksky.dropbox.defs", ··· 5099 AppRockskyPlayerGetPlaybackQueue: "app.rocksky.player.getPlaybackQueue", 5100 AppRockskyPlayerNext: "app.rocksky.player.next", 5101 AppRockskyPlayerPause: "app.rocksky.player.pause", 5102 AppRockskyPlayerPlayDirectory: "app.rocksky.player.playDirectory", 5103 AppRockskyPlayerPlayFile: "app.rocksky.player.playFile", 5104 - AppRockskyPlayerPlay: "app.rocksky.player.play", 5105 AppRockskyPlayerPrevious: "app.rocksky.player.previous", 5106 AppRockskyPlayerSeek: "app.rocksky.player.seek", 5107 AppRockskyPlaylistCreatePlaylist: "app.rocksky.playlist.createPlaylist",
··· 1267 }, 1268 }, 1269 }, 1270 + AppRockskyAlbumGetAlbumTracks: { 1271 lexicon: 1, 1272 + id: "app.rocksky.album.getAlbumTracks", 1273 defs: { 1274 main: { 1275 type: "query", 1276 + description: "Get tracks for an album", 1277 parameters: { 1278 type: "params", 1279 + required: ["uri"], 1280 properties: { 1281 + uri: { 1282 + type: "string", 1283 + description: "The URI of the album to retrieve tracks from", 1284 + format: "at-uri", 1285 }, 1286 }, 1287 }, ··· 1290 schema: { 1291 type: "object", 1292 properties: { 1293 + tracks: { 1294 type: "array", 1295 items: { 1296 type: "ref", 1297 + ref: "lex:app.rocksky.song.defs#songViewBasic", 1298 }, 1299 }, 1300 }, ··· 1303 }, 1304 }, 1305 }, 1306 + AppRockskyAlbumGetAlbums: { 1307 lexicon: 1, 1308 + id: "app.rocksky.album.getAlbums", 1309 defs: { 1310 main: { 1311 type: "query", 1312 + description: "Get albums", 1313 parameters: { 1314 type: "params", 1315 properties: { 1316 + limit: { 1317 + type: "integer", 1318 + description: "The maximum number of albums to return", 1319 + minimum: 1, 1320 + }, 1321 + offset: { 1322 + type: "integer", 1323 + description: "The offset for pagination", 1324 + minimum: 0, 1325 }, 1326 }, 1327 }, ··· 1330 schema: { 1331 type: "object", 1332 properties: { 1333 + albums: { 1334 type: "array", 1335 items: { 1336 type: "ref", 1337 + ref: "lex:app.rocksky.album.defs#albumViewBasic", 1338 }, 1339 }, 1340 }, ··· 1737 }, 1738 }, 1739 }, 1740 + AppRockskyArtistGetArtist: { 1741 + lexicon: 1, 1742 + id: "app.rocksky.artist.getArtist", 1743 + defs: { 1744 + main: { 1745 + type: "query", 1746 + description: "Get artist details", 1747 + parameters: { 1748 + type: "params", 1749 + required: ["uri"], 1750 + properties: { 1751 + uri: { 1752 + type: "string", 1753 + description: "The URI of the artist to retrieve details from", 1754 + format: "at-uri", 1755 + }, 1756 + }, 1757 + }, 1758 + output: { 1759 + encoding: "application/json", 1760 + schema: { 1761 + type: "ref", 1762 + ref: "lex:app.rocksky.artist.defs#artistViewDetailed", 1763 + }, 1764 + }, 1765 + }, 1766 + }, 1767 + }, 1768 AppRockskyArtistGetArtistAlbums: { 1769 lexicon: 1, 1770 id: "app.rocksky.artist.getArtistAlbums", ··· 1796 }, 1797 }, 1798 }, 1799 }, 1800 }, 1801 }, ··· 1844 }, 1845 }, 1846 }, 1847 + AppRockskyArtistGetArtistTracks: { 1848 lexicon: 1, 1849 + id: "app.rocksky.artist.getArtistTracks", 1850 defs: { 1851 main: { 1852 type: "query", 1853 + description: "Get artist's tracks", 1854 parameters: { 1855 type: "params", 1856 properties: { 1857 + uri: { 1858 + type: "string", 1859 + description: "The URI of the artist to retrieve albums from", 1860 + format: "at-uri", 1861 + }, 1862 limit: { 1863 type: "integer", 1864 + description: "The maximum number of tracks to return", 1865 minimum: 1, 1866 }, 1867 offset: { ··· 1876 schema: { 1877 type: "object", 1878 properties: { 1879 + tracks: { 1880 type: "array", 1881 items: { 1882 type: "ref", 1883 + ref: "lex:app.rocksky.song.defs#songViewBasic", 1884 }, 1885 }, 1886 }, ··· 1889 }, 1890 }, 1891 }, 1892 + AppRockskyArtistGetArtists: { 1893 lexicon: 1, 1894 + id: "app.rocksky.artist.getArtists", 1895 defs: { 1896 main: { 1897 type: "query", 1898 + description: "Get artists", 1899 parameters: { 1900 type: "params", 1901 properties: { 1902 limit: { 1903 type: "integer", 1904 + description: "The maximum number of artists to return", 1905 minimum: 1, 1906 }, 1907 offset: { 1908 type: "integer", 1909 description: "The offset for pagination", 1910 minimum: 0, 1911 + }, 1912 + names: { 1913 + type: "string", 1914 + description: "The names of the artists to return", 1915 }, 1916 }, 1917 }, ··· 1920 schema: { 1921 type: "object", 1922 properties: { 1923 + artists: { 1924 type: "array", 1925 items: { 1926 type: "ref", 1927 + ref: "lex:app.rocksky.artist.defs#artistViewBasic", 1928 }, 1929 }, 1930 }, ··· 2769 }, 2770 }, 2771 }, 2772 + AppRockskyPlayerPlay: { 2773 + lexicon: 1, 2774 + id: "app.rocksky.player.play", 2775 + defs: { 2776 + main: { 2777 + type: "procedure", 2778 + description: "Resume playback of the currently paused track", 2779 + parameters: { 2780 + type: "params", 2781 + properties: { 2782 + playerId: { 2783 + type: "string", 2784 + }, 2785 + }, 2786 + }, 2787 + }, 2788 + }, 2789 + }, 2790 AppRockskyPlayerPlayDirectory: { 2791 lexicon: 1, 2792 id: "app.rocksky.player.playDirectory", ··· 2833 type: "string", 2834 }, 2835 fileId: { 2836 type: "string", 2837 }, 2838 }, ··· 5061 AppRockskyAlbum: "app.rocksky.album", 5062 AppRockskyAlbumDefs: "app.rocksky.album.defs", 5063 AppRockskyAlbumGetAlbum: "app.rocksky.album.getAlbum", 5064 AppRockskyAlbumGetAlbumTracks: "app.rocksky.album.getAlbumTracks", 5065 + AppRockskyAlbumGetAlbums: "app.rocksky.album.getAlbums", 5066 AppRockskyApikeyCreateApikey: "app.rocksky.apikey.createApikey", 5067 AppRockskyApikeyDefs: "app.rocksky.apikey.defs", 5068 AppRockskyApikeysDefs: "app.rocksky.apikeys.defs", ··· 5071 AppRockskyApikeyUpdateApikey: "app.rocksky.apikey.updateApikey", 5072 AppRockskyArtist: "app.rocksky.artist", 5073 AppRockskyArtistDefs: "app.rocksky.artist.defs", 5074 AppRockskyArtistGetArtist: "app.rocksky.artist.getArtist", 5075 + AppRockskyArtistGetArtistAlbums: "app.rocksky.artist.getArtistAlbums", 5076 AppRockskyArtistGetArtistListeners: "app.rocksky.artist.getArtistListeners", 5077 AppRockskyArtistGetArtistTracks: "app.rocksky.artist.getArtistTracks", 5078 + AppRockskyArtistGetArtists: "app.rocksky.artist.getArtists", 5079 AppRockskyChartsDefs: "app.rocksky.charts.defs", 5080 AppRockskyChartsGetScrobblesChart: "app.rocksky.charts.getScrobblesChart", 5081 AppRockskyDropboxDefs: "app.rocksky.dropbox.defs", ··· 5102 AppRockskyPlayerGetPlaybackQueue: "app.rocksky.player.getPlaybackQueue", 5103 AppRockskyPlayerNext: "app.rocksky.player.next", 5104 AppRockskyPlayerPause: "app.rocksky.player.pause", 5105 + AppRockskyPlayerPlay: "app.rocksky.player.play", 5106 AppRockskyPlayerPlayDirectory: "app.rocksky.player.playDirectory", 5107 AppRockskyPlayerPlayFile: "app.rocksky.player.playFile", 5108 AppRockskyPlayerPrevious: "app.rocksky.player.previous", 5109 AppRockskyPlayerSeek: "app.rocksky.player.seek", 5110 AppRockskyPlaylistCreatePlaylist: "app.rocksky.playlist.createPlaylist",
+2
apps/api/src/lexicon/types/app/rocksky/artist/getArtists.ts
··· 14 limit?: number; 15 /** The offset for pagination */ 16 offset?: number; 17 } 18 19 export type InputSchema = undefined;
··· 14 limit?: number; 15 /** The offset for pagination */ 16 offset?: number; 17 + /** The names of the artists to return */ 18 + names?: string; 19 } 20 21 export type InputSchema = undefined;
+46 -46
apps/api/src/nowplaying/nowplaying.service.ts
··· 26 27 export async function putArtistRecord( 28 track: Track, 29 - agent: Agent 30 ): Promise<string | null> { 31 const rkey = TID.nextStr(); 32 const record: Artist.Record = { ··· 62 63 export async function putAlbumRecord( 64 track: Track, 65 - agent: Agent 66 ): Promise<string | null> { 67 const rkey = TID.nextStr(); 68 ··· 103 104 export async function putSongRecord( 105 track: Track, 106 - agent: Agent 107 ): Promise<string | null> { 108 const rkey = TID.nextStr(); 109 ··· 158 159 async function putScrobbleRecord( 160 track: Track, 161 - agent: Agent 162 ): Promise<string | null> { 163 const rkey = TID.nextStr(); 164 ··· 276 .where( 277 and( 278 eq(artistAlbums.albumId, scrobble.album.id), 279 - eq(artistAlbums.artistId, scrobble.artist.id) 280 - ) 281 ) 282 .limit(1) 283 .then((rows) => rows[0]), ··· 440 }, 441 }), 442 null, 443 - 2 444 ); 445 446 ctx.nc.publish( 447 "rocksky.scrobble", 448 - Buffer.from(message.replaceAll("sha_256", "sha256")) 449 ); 450 451 const trackMessage = JSON.stringify( ··· 492 xata_createdat: artist_album.createdAt.toISOString(), 493 xata_updatedat: artist_album.updatedAt.toISOString(), 494 }, 495 - }) 496 ); 497 498 ctx.nc.publish( 499 "rocksky.track", 500 - Buffer.from(trackMessage.replaceAll("sha_256", "sha256")) 501 ); 502 } 503 ··· 505 ctx: Context, 506 track: Track, 507 agent: Agent, 508 - userDid: string 509 ): Promise<void> { 510 // check if scrobble already exists (user did + timestamp) 511 const scrobbleTime = dayjs.unix(track.timestamp || dayjs().unix()); ··· 524 eq(tracks.title, track.title), 525 eq(tracks.artist, track.artist), 526 gte(scrobbles.timestamp, scrobbleTime.subtract(60, "seconds").toDate()), 527 - lte(scrobbles.timestamp, scrobbleTime.add(60, "seconds").toDate()) 528 - ) 529 ) 530 .limit(1) 531 .then((rows) => rows[0]); ··· 533 if (existingScrobble) { 534 console.log( 535 `Scrobble already exists for ${chalk.cyan(track.title)} at ${chalk.cyan( 536 - scrobbleTime.format("YYYY-MM-DD HH:mm:ss") 537 - )}` 538 ); 539 return; 540 } ··· 547 tracks.sha256, 548 createHash("sha256") 549 .update( 550 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 551 ) 552 - .digest("hex") 553 - ) 554 ) 555 .limit(1) 556 .then((rows) => rows[0]); ··· 564 albums.sha256, 565 createHash("sha256") 566 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 567 - .digest("hex") 568 - ) 569 ) 570 .limit(1) 571 .then((rows) => rows[0]); ··· 586 artists.sha256, 587 createHash("sha256") 588 .update(track.albumArtist.toLowerCase()) 589 - .digest("hex") 590 - ) 591 ) 592 .limit(1) 593 .then((rows) => rows[0]); ··· 618 artist: track.artist.split(",").map((a) => ({ name: a.trim() })), 619 name: track.title, 620 album: track.album, 621 - } 622 ); 623 624 if (!mbTrack?.trackMBID) { ··· 647 albums.sha256, 648 createHash("sha256") 649 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 650 - .digest("hex") 651 - ) 652 ) 653 .limit(1) 654 .then((rows) => rows[0]); ··· 664 tracks.sha256, 665 createHash("sha256") 666 .update( 667 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 668 ) 669 - .digest("hex") 670 - ) 671 ) 672 .limit(1) 673 .then((rows) => rows[0]); ··· 681 682 if (existingTrack) { 683 console.log( 684 - `Song found: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 685 ); 686 } 687 ··· 694 artists.sha256, 695 createHash("sha256") 696 .update(track.albumArtist.toLowerCase()) 697 - .digest("hex") 698 ), 699 eq( 700 artists.sha256, 701 - createHash("sha256").update(track.artist.toLowerCase()).digest("hex") 702 - ) 703 - ) 704 ) 705 .limit(1) 706 .then((rows) => rows[0]); ··· 715 .innerJoin(artists, eq(userArtists.artistId, artists.id)) 716 .innerJoin(users, eq(userArtists.userId, users.id)) 717 .where( 718 - and(eq(artists.id, existingArtist?.id || ""), eq(users.did, userDid)) 719 ) 720 .limit(1) 721 .then((rows) => rows[0]); ··· 750 tracks.sha256, 751 createHash("sha256") 752 .update( 753 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 754 ) 755 - .digest("hex") 756 - ) 757 ) 758 .limit(1) 759 .then((rows) => rows[0]); 760 761 while (!existingTrack?.artistUri && !existingTrack?.albumUri && tries < 30) { 762 console.log( 763 - `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}` 764 ); 765 existingTrack = await ctx.db 766 .select() ··· 770 tracks.sha256, 771 createHash("sha256") 772 .update( 773 - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 774 ) 775 - .digest("hex") 776 - ) 777 ) 778 .limit(1) 779 .then((rows) => rows[0]); ··· 788 artists.sha256, 789 createHash("sha256") 790 .update(track.albumArtist.toLowerCase()) 791 - .digest("hex") 792 - ) 793 ) 794 .limit(1) 795 .then((rows) => rows[0]); ··· 812 albums.sha256, 813 createHash("sha256") 814 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 815 - .digest("hex") 816 - ) 817 ) 818 .limit(1) 819 .then((rows) => rows[0]); ··· 843 844 if (existingTrack?.artistUri) { 845 console.log( 846 - `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 847 ); 848 } 849
··· 26 27 export async function putArtistRecord( 28 track: Track, 29 + agent: Agent, 30 ): Promise<string | null> { 31 const rkey = TID.nextStr(); 32 const record: Artist.Record = { ··· 62 63 export async function putAlbumRecord( 64 track: Track, 65 + agent: Agent, 66 ): Promise<string | null> { 67 const rkey = TID.nextStr(); 68 ··· 103 104 export async function putSongRecord( 105 track: Track, 106 + agent: Agent, 107 ): Promise<string | null> { 108 const rkey = TID.nextStr(); 109 ··· 158 159 async function putScrobbleRecord( 160 track: Track, 161 + agent: Agent, 162 ): Promise<string | null> { 163 const rkey = TID.nextStr(); 164 ··· 276 .where( 277 and( 278 eq(artistAlbums.albumId, scrobble.album.id), 279 + eq(artistAlbums.artistId, scrobble.artist.id), 280 + ), 281 ) 282 .limit(1) 283 .then((rows) => rows[0]), ··· 440 }, 441 }), 442 null, 443 + 2, 444 ); 445 446 ctx.nc.publish( 447 "rocksky.scrobble", 448 + Buffer.from(message.replaceAll("sha_256", "sha256")), 449 ); 450 451 const trackMessage = JSON.stringify( ··· 492 xata_createdat: artist_album.createdAt.toISOString(), 493 xata_updatedat: artist_album.updatedAt.toISOString(), 494 }, 495 + }), 496 ); 497 498 ctx.nc.publish( 499 "rocksky.track", 500 + Buffer.from(trackMessage.replaceAll("sha_256", "sha256")), 501 ); 502 } 503 ··· 505 ctx: Context, 506 track: Track, 507 agent: Agent, 508 + userDid: string, 509 ): Promise<void> { 510 // check if scrobble already exists (user did + timestamp) 511 const scrobbleTime = dayjs.unix(track.timestamp || dayjs().unix()); ··· 524 eq(tracks.title, track.title), 525 eq(tracks.artist, track.artist), 526 gte(scrobbles.timestamp, scrobbleTime.subtract(60, "seconds").toDate()), 527 + lte(scrobbles.timestamp, scrobbleTime.add(60, "seconds").toDate()), 528 + ), 529 ) 530 .limit(1) 531 .then((rows) => rows[0]); ··· 533 if (existingScrobble) { 534 console.log( 535 `Scrobble already exists for ${chalk.cyan(track.title)} at ${chalk.cyan( 536 + scrobbleTime.format("YYYY-MM-DD HH:mm:ss"), 537 + )}`, 538 ); 539 return; 540 } ··· 547 tracks.sha256, 548 createHash("sha256") 549 .update( 550 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 551 ) 552 + .digest("hex"), 553 + ), 554 ) 555 .limit(1) 556 .then((rows) => rows[0]); ··· 564 albums.sha256, 565 createHash("sha256") 566 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 567 + .digest("hex"), 568 + ), 569 ) 570 .limit(1) 571 .then((rows) => rows[0]); ··· 586 artists.sha256, 587 createHash("sha256") 588 .update(track.albumArtist.toLowerCase()) 589 + .digest("hex"), 590 + ), 591 ) 592 .limit(1) 593 .then((rows) => rows[0]); ··· 618 artist: track.artist.split(",").map((a) => ({ name: a.trim() })), 619 name: track.title, 620 album: track.album, 621 + }, 622 ); 623 624 if (!mbTrack?.trackMBID) { ··· 647 albums.sha256, 648 createHash("sha256") 649 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 650 + .digest("hex"), 651 + ), 652 ) 653 .limit(1) 654 .then((rows) => rows[0]); ··· 664 tracks.sha256, 665 createHash("sha256") 666 .update( 667 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 668 ) 669 + .digest("hex"), 670 + ), 671 ) 672 .limit(1) 673 .then((rows) => rows[0]); ··· 681 682 if (existingTrack) { 683 console.log( 684 + `Song found: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, 685 ); 686 } 687 ··· 694 artists.sha256, 695 createHash("sha256") 696 .update(track.albumArtist.toLowerCase()) 697 + .digest("hex"), 698 ), 699 eq( 700 artists.sha256, 701 + createHash("sha256").update(track.artist.toLowerCase()).digest("hex"), 702 + ), 703 + ), 704 ) 705 .limit(1) 706 .then((rows) => rows[0]); ··· 715 .innerJoin(artists, eq(userArtists.artistId, artists.id)) 716 .innerJoin(users, eq(userArtists.userId, users.id)) 717 .where( 718 + and(eq(artists.id, existingArtist?.id || ""), eq(users.did, userDid)), 719 ) 720 .limit(1) 721 .then((rows) => rows[0]); ··· 750 tracks.sha256, 751 createHash("sha256") 752 .update( 753 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 754 ) 755 + .digest("hex"), 756 + ), 757 ) 758 .limit(1) 759 .then((rows) => rows[0]); 760 761 while (!existingTrack?.artistUri && !existingTrack?.albumUri && tries < 30) { 762 console.log( 763 + `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}`, 764 ); 765 existingTrack = await ctx.db 766 .select() ··· 770 tracks.sha256, 771 createHash("sha256") 772 .update( 773 + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), 774 ) 775 + .digest("hex"), 776 + ), 777 ) 778 .limit(1) 779 .then((rows) => rows[0]); ··· 788 artists.sha256, 789 createHash("sha256") 790 .update(track.albumArtist.toLowerCase()) 791 + .digest("hex"), 792 + ), 793 ) 794 .limit(1) 795 .then((rows) => rows[0]); ··· 812 albums.sha256, 813 createHash("sha256") 814 .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) 815 + .digest("hex"), 816 + ), 817 ) 818 .limit(1) 819 .then((rows) => rows[0]); ··· 843 844 if (existingTrack?.artistUri) { 845 console.log( 846 + `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, 847 ); 848 } 849
+1 -3
apps/api/src/schema/spotify-accounts.ts
··· 9 import users from "./users"; 10 11 const spotifyAccounts = pgTable("spotify_accounts", { 12 - id: text("xata_id") 13 - .primaryKey() 14 - .default(sql`xata_id()`), 15 xataVersion: integer("xata_version"), 16 email: text("email").notNull(), 17 userId: text("user_id")
··· 9 import users from "./users"; 10 11 const spotifyAccounts = pgTable("spotify_accounts", { 12 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 13 xataVersion: integer("xata_version"), 14 email: text("email").notNull(), 15 userId: text("user_id")
+1 -3
apps/api/src/schema/spotify-apps.ts
··· 2 import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 4 const spotifyApps = pgTable("spotify_apps", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 8 xataVersion: integer("xata_version"), 9 spotifyAppId: text("spotify_app_id").unique().notNull(), 10 spotifySecret: text("spotify_secret").notNull(),
··· 2 import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 4 const spotifyApps = pgTable("spotify_apps", { 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 6 xataVersion: integer("xata_version"), 7 spotifyAppId: text("spotify_app_id").unique().notNull(), 8 spotifySecret: text("spotify_secret").notNull(),
+1 -3
apps/api/src/schema/spotify-tokens.ts
··· 3 import users from "./users"; 4 5 const spotifyTokens = pgTable("spotify_tokens", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 9 xataVersion: integer("xata_version"), 10 accessToken: text("access_token").notNull(), 11 refreshToken: text("refresh_token").notNull(),
··· 3 import users from "./users"; 4 5 const spotifyTokens = pgTable("spotify_tokens", { 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 7 xataVersion: integer("xata_version"), 8 accessToken: text("access_token").notNull(), 9 refreshToken: text("refresh_token").notNull(),
+6 -6
apps/api/src/scripts/genres.ts
··· 11 .from(tables.spotifyTokens) 12 .leftJoin( 13 tables.spotifyAccounts, 14 - eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId) 15 ) 16 .leftJoin( 17 tables.spotifyApps, 18 - eq(tables.spotifyApps.spotifyAppId, tables.spotifyTokens.spotifyAppId) 19 ) 20 .where(eq(tables.spotifyAccounts.isBetaUser, true)) 21 .execute(); ··· 24 spotifyTokens[Math.floor(Math.random() * spotifyTokens.length)]; 25 const refreshToken = decrypt( 26 record.spotify_tokens.refreshToken, 27 - env.SPOTIFY_ENCRYPTION_KEY 28 ); 29 30 const accessToken = await fetch("https://accounts.spotify.com/api/token", { ··· 38 client_id: record.spotify_apps.spotifyAppId, 39 client_secret: decrypt( 40 record.spotify_apps.spotifySecret, 41 - env.SPOTIFY_ENCRYPTION_KEY 42 ), 43 }), 44 }) ··· 60 headers: { 61 Authorization: `Bearer ${token}`, 62 }, 63 - } 64 ) 65 .then( 66 (res) => ··· 73 images: Array<{ url: string }>; 74 }>; 75 }; 76 - }> 77 ) 78 .then(async (data) => _.get(data, "artists.items.0")); 79
··· 11 .from(tables.spotifyTokens) 12 .leftJoin( 13 tables.spotifyAccounts, 14 + eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId), 15 ) 16 .leftJoin( 17 tables.spotifyApps, 18 + eq(tables.spotifyApps.spotifyAppId, tables.spotifyTokens.spotifyAppId), 19 ) 20 .where(eq(tables.spotifyAccounts.isBetaUser, true)) 21 .execute(); ··· 24 spotifyTokens[Math.floor(Math.random() * spotifyTokens.length)]; 25 const refreshToken = decrypt( 26 record.spotify_tokens.refreshToken, 27 + env.SPOTIFY_ENCRYPTION_KEY, 28 ); 29 30 const accessToken = await fetch("https://accounts.spotify.com/api/token", { ··· 38 client_id: record.spotify_apps.spotifyAppId, 39 client_secret: decrypt( 40 record.spotify_apps.spotifySecret, 41 + env.SPOTIFY_ENCRYPTION_KEY, 42 ), 43 }), 44 }) ··· 60 headers: { 61 Authorization: `Bearer ${token}`, 62 }, 63 + }, 64 ) 65 .then( 66 (res) => ··· 73 images: Array<{ url: string }>; 74 }>; 75 }; 76 + }>, 77 ) 78 .then(async (data) => _.get(data, "artists.items.0")); 79
+2 -2
apps/api/src/scripts/spotify.ts
··· 10 11 if (!clientId || !clientSecret) { 12 console.error( 13 - "Please provide Spotify Client ID and Client Secret as command line arguments" 14 ); 15 console.log( 16 - chalk.greenBright("Usage: ts-node spotify.ts <client_id> <client_secret>") 17 ); 18 process.exit(1); 19 }
··· 10 11 if (!clientId || !clientSecret) { 12 console.error( 13 + "Please provide Spotify Client ID and Client Secret as command line arguments", 14 ); 15 console.log( 16 + chalk.greenBright("Usage: ts-node spotify.ts <client_id> <client_secret>"), 17 ); 18 process.exit(1); 19 }
+1 -1
apps/api/src/scripts/sync.ts
··· 143 } catch (err) { 144 console.error( 145 `Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`, 146 - err 147 ); 148 } 149 }
··· 143 } catch (err) { 144 console.error( 145 `Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`, 146 + err, 147 ); 148 } 149 }
+1 -1
apps/api/src/server.ts
··· 31 32 app.listen(process.env.ROCKSKY_XPRC_PORT || 3004, () => { 33 console.log( 34 - `Rocksky XRPC API is running on port ${process.env.ROCKSKY_XRPC_PORT || 3004}` 35 ); 36 });
··· 31 32 app.listen(process.env.ROCKSKY_XPRC_PORT || 3004, () => { 33 console.log( 34 + `Rocksky XRPC API is running on port ${process.env.ROCKSKY_XRPC_PORT || 3004}`, 35 ); 36 });
+30 -30
apps/api/src/spotify/app.ts
··· 24 limit: 10, // max Spotify API calls 25 window: 15, // per 10 seconds 26 keyPrefix: "spotify-ratelimit", 27 - }) 28 ); 29 30 app.get("/login", async (c) => { ··· 58 .leftJoin(users, eq(spotifyAccounts.userId, users.id)) 59 .leftJoin( 60 spotifyApps, 61 - eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId) 62 ) 63 .where( 64 and( 65 eq(spotifyAccounts.userId, user.id), 66 - eq(spotifyAccounts.isBetaUser, true) 67 - ) 68 ) 69 .limit(1) 70 .then((rows) => rows[0]); ··· 74 const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${spotifyAccount?.spotify_apps?.spotifyAppId}&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}`; 75 c.header( 76 "Set-Cookie", 77 - `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure` 78 ); 79 return c.json({ redirectUrl }); 80 }); ··· 110 .from(spotifyAccounts) 111 .leftJoin( 112 spotifyApps, 113 - eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId) 114 ) 115 .where( 116 and( 117 eq(spotifyAccounts.userId, user.id), 118 - eq(spotifyAccounts.isBetaUser, true) 119 - ) 120 ) 121 .limit(1) 122 .then((rows) => rows[0]); ··· 179 .where( 180 and( 181 eq(spotifyAccounts.userId, user.id), 182 - eq(spotifyAccounts.isBetaUser, true) 183 - ) 184 ) 185 .limit(1) 186 .then((rows) => rows[0]); ··· 230 appId: spotifyApps.id, 231 spotifyAppId: spotifyApps.spotifyAppId, 232 accountCount: sql<number>`COUNT(${spotifyAccounts.id})`.as( 233 - "account_count" 234 ), 235 }) 236 .from(spotifyApps) ··· 311 } 312 313 const cached = await ctx.redis.get( 314 - `${spotifyAccount.spotifyAccount.email}:current` 315 ); 316 if (!cached) { 317 return c.json({}); ··· 321 322 const sha256 = createHash("sha256") 323 .update( 324 - `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase() 325 ) 326 .digest("hex"); 327 ··· 385 .from(spotifyTokens) 386 .leftJoin( 387 spotifyApps, 388 - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) 389 ) 390 .where(eq(spotifyTokens.userId, user.id)) 391 .limit(1) ··· 398 399 const refreshToken = decrypt( 400 spotifyToken.spotify_tokens.refreshToken, 401 - env.SPOTIFY_ENCRYPTION_KEY 402 ); 403 404 // get new access token ··· 413 client_id: spotifyToken.spotify_apps.spotifyAppId, 414 client_secret: decrypt( 415 spotifyToken.spotify_apps.spotifySecret, 416 - env.SPOTIFY_ENCRYPTION_KEY 417 ), 418 }), 419 }); ··· 468 .from(spotifyTokens) 469 .leftJoin( 470 spotifyApps, 471 - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) 472 ) 473 .where(eq(spotifyTokens.userId, user.id)) 474 .limit(1) ··· 481 482 const refreshToken = decrypt( 483 spotifyToken.spotify_tokens.refreshToken, 484 - env.SPOTIFY_ENCRYPTION_KEY 485 ); 486 487 // get new access token ··· 496 client_id: spotifyToken.spotify_apps.spotifyAppId, 497 client_secret: decrypt( 498 spotifyToken.spotify_apps.spotifySecret, 499 - env.SPOTIFY_ENCRYPTION_KEY 500 ), 501 }), 502 }); ··· 551 .from(spotifyTokens) 552 .leftJoin( 553 spotifyApps, 554 - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) 555 ) 556 .where(eq(spotifyTokens.userId, user.id)) 557 .limit(1) ··· 564 565 const refreshToken = decrypt( 566 spotifyToken.spotify_tokens.refreshToken, 567 - env.SPOTIFY_ENCRYPTION_KEY 568 ); 569 570 // get new access token ··· 579 client_id: spotifyToken.spotify_apps.spotifyAppId, 580 client_secret: decrypt( 581 spotifyToken.spotify_apps.spotifySecret, 582 - env.SPOTIFY_ENCRYPTION_KEY 583 ), 584 }), 585 }); ··· 634 .from(spotifyTokens) 635 .leftJoin( 636 spotifyApps, 637 - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) 638 ) 639 .where(eq(spotifyTokens.userId, user.id)) 640 .limit(1) ··· 647 648 const refreshToken = decrypt( 649 spotifyToken.spotify_tokens.refreshToken, 650 - env.SPOTIFY_ENCRYPTION_KEY 651 ); 652 653 // get new access token ··· 662 client_id: spotifyToken.spotify_apps.spotifyAppId, 663 client_secret: decrypt( 664 spotifyToken.spotify_apps.spotifySecret, 665 - env.SPOTIFY_ENCRYPTION_KEY 666 ), 667 }), 668 }); ··· 678 headers: { 679 Authorization: `Bearer ${access_token}`, 680 }, 681 - } 682 ); 683 684 if (response.status === 403) { ··· 720 .from(spotifyTokens) 721 .leftJoin( 722 spotifyApps, 723 - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) 724 ) 725 .where(eq(spotifyTokens.userId, user.id)) 726 .limit(1) ··· 733 734 const refreshToken = decrypt( 735 spotifyToken.spotify_tokens.refreshToken, 736 - env.SPOTIFY_ENCRYPTION_KEY 737 ); 738 739 // get new access token ··· 748 client_id: spotifyToken.spotify_apps.spotifyAppId, 749 client_secret: decrypt( 750 spotifyToken.spotify_apps.spotifySecret, 751 - env.SPOTIFY_ENCRYPTION_KEY 752 ), 753 }), 754 }); ··· 765 headers: { 766 Authorization: `Bearer ${access_token}`, 767 }, 768 - } 769 ); 770 771 if (response.status === 403) {
··· 24 limit: 10, // max Spotify API calls 25 window: 15, // per 10 seconds 26 keyPrefix: "spotify-ratelimit", 27 + }), 28 ); 29 30 app.get("/login", async (c) => { ··· 58 .leftJoin(users, eq(spotifyAccounts.userId, users.id)) 59 .leftJoin( 60 spotifyApps, 61 + eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId), 62 ) 63 .where( 64 and( 65 eq(spotifyAccounts.userId, user.id), 66 + eq(spotifyAccounts.isBetaUser, true), 67 + ), 68 ) 69 .limit(1) 70 .then((rows) => rows[0]); ··· 74 const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${spotifyAccount?.spotify_apps?.spotifyAppId}&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}`; 75 c.header( 76 "Set-Cookie", 77 + `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`, 78 ); 79 return c.json({ redirectUrl }); 80 }); ··· 110 .from(spotifyAccounts) 111 .leftJoin( 112 spotifyApps, 113 + eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId), 114 ) 115 .where( 116 and( 117 eq(spotifyAccounts.userId, user.id), 118 + eq(spotifyAccounts.isBetaUser, true), 119 + ), 120 ) 121 .limit(1) 122 .then((rows) => rows[0]); ··· 179 .where( 180 and( 181 eq(spotifyAccounts.userId, user.id), 182 + eq(spotifyAccounts.isBetaUser, true), 183 + ), 184 ) 185 .limit(1) 186 .then((rows) => rows[0]); ··· 230 appId: spotifyApps.id, 231 spotifyAppId: spotifyApps.spotifyAppId, 232 accountCount: sql<number>`COUNT(${spotifyAccounts.id})`.as( 233 + "account_count", 234 ), 235 }) 236 .from(spotifyApps) ··· 311 } 312 313 const cached = await ctx.redis.get( 314 + `${spotifyAccount.spotifyAccount.email}:current`, 315 ); 316 if (!cached) { 317 return c.json({}); ··· 321 322 const sha256 = createHash("sha256") 323 .update( 324 + `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase(), 325 ) 326 .digest("hex"); 327 ··· 385 .from(spotifyTokens) 386 .leftJoin( 387 spotifyApps, 388 + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), 389 ) 390 .where(eq(spotifyTokens.userId, user.id)) 391 .limit(1) ··· 398 399 const refreshToken = decrypt( 400 spotifyToken.spotify_tokens.refreshToken, 401 + env.SPOTIFY_ENCRYPTION_KEY, 402 ); 403 404 // get new access token ··· 413 client_id: spotifyToken.spotify_apps.spotifyAppId, 414 client_secret: decrypt( 415 spotifyToken.spotify_apps.spotifySecret, 416 + env.SPOTIFY_ENCRYPTION_KEY, 417 ), 418 }), 419 }); ··· 468 .from(spotifyTokens) 469 .leftJoin( 470 spotifyApps, 471 + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), 472 ) 473 .where(eq(spotifyTokens.userId, user.id)) 474 .limit(1) ··· 481 482 const refreshToken = decrypt( 483 spotifyToken.spotify_tokens.refreshToken, 484 + env.SPOTIFY_ENCRYPTION_KEY, 485 ); 486 487 // get new access token ··· 496 client_id: spotifyToken.spotify_apps.spotifyAppId, 497 client_secret: decrypt( 498 spotifyToken.spotify_apps.spotifySecret, 499 + env.SPOTIFY_ENCRYPTION_KEY, 500 ), 501 }), 502 }); ··· 551 .from(spotifyTokens) 552 .leftJoin( 553 spotifyApps, 554 + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), 555 ) 556 .where(eq(spotifyTokens.userId, user.id)) 557 .limit(1) ··· 564 565 const refreshToken = decrypt( 566 spotifyToken.spotify_tokens.refreshToken, 567 + env.SPOTIFY_ENCRYPTION_KEY, 568 ); 569 570 // get new access token ··· 579 client_id: spotifyToken.spotify_apps.spotifyAppId, 580 client_secret: decrypt( 581 spotifyToken.spotify_apps.spotifySecret, 582 + env.SPOTIFY_ENCRYPTION_KEY, 583 ), 584 }), 585 }); ··· 634 .from(spotifyTokens) 635 .leftJoin( 636 spotifyApps, 637 + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), 638 ) 639 .where(eq(spotifyTokens.userId, user.id)) 640 .limit(1) ··· 647 648 const refreshToken = decrypt( 649 spotifyToken.spotify_tokens.refreshToken, 650 + env.SPOTIFY_ENCRYPTION_KEY, 651 ); 652 653 // get new access token ··· 662 client_id: spotifyToken.spotify_apps.spotifyAppId, 663 client_secret: decrypt( 664 spotifyToken.spotify_apps.spotifySecret, 665 + env.SPOTIFY_ENCRYPTION_KEY, 666 ), 667 }), 668 }); ··· 678 headers: { 679 Authorization: `Bearer ${access_token}`, 680 }, 681 + }, 682 ); 683 684 if (response.status === 403) { ··· 720 .from(spotifyTokens) 721 .leftJoin( 722 spotifyApps, 723 + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), 724 ) 725 .where(eq(spotifyTokens.userId, user.id)) 726 .limit(1) ··· 733 734 const refreshToken = decrypt( 735 spotifyToken.spotify_tokens.refreshToken, 736 + env.SPOTIFY_ENCRYPTION_KEY, 737 ); 738 739 // get new access token ··· 748 client_id: spotifyToken.spotify_apps.spotifyAppId, 749 client_secret: decrypt( 750 spotifyToken.spotify_apps.spotifySecret, 751 + env.SPOTIFY_ENCRYPTION_KEY, 752 ), 753 }), 754 }); ··· 765 headers: { 766 Authorization: `Bearer ${access_token}`, 767 }, 768 + }, 769 ); 770 771 if (response.status === 403) {
+6 -8
apps/api/src/tealfm/index.ts
··· 35 record?.trackName === track.name)) && 36 // diff in seconds less than 60 37 Math.abs( 38 - new Date(record.playedTime).getTime() - 39 - new Date(track.timestamp).getTime(), 40 - ) < 60000 41 ); 42 }); 43 if (alreadyPlayed) { 44 console.log( 45 - `Track ${chalk.cyan(track.name)} by ${ 46 - chalk.cyan( 47 - track.artist.map((a) => a.name).join(", "), 48 - ) 49 - } already played recently. Skipping...`, 50 ); 51 return; 52 }
··· 35 record?.trackName === track.name)) && 36 // diff in seconds less than 60 37 Math.abs( 38 + new Date(record.playedTime).getTime() - 39 + new Date(track.timestamp).getTime(), 40 + ) < 60000 41 ); 42 }); 43 if (alreadyPlayed) { 44 console.log( 45 + `Track ${chalk.cyan(track.name)} by ${chalk.cyan( 46 + track.artist.map((a) => a.name).join(", "), 47 + )} already played recently. Skipping...`, 48 ); 49 return; 50 }
+9 -9
apps/api/src/xrpc/app/rocksky/actor/getProfile.ts
··· 33 Effect.catchAll((err) => { 34 console.error(err); 35 return Effect.succeed({}); 36 - }) 37 ); 38 server.app.rocksky.actor.getProfile({ 39 auth: ctx.authVerifier, ··· 194 .from(tables.spotifyAccounts) 195 .leftJoin( 196 tables.users, 197 - eq(tables.spotifyAccounts.userId, tables.users.id) 198 ) 199 .where(eq(tables.users.did, did)) 200 .execute() ··· 204 .from(tables.spotifyTokens) 205 .leftJoin( 206 tables.users, 207 - eq(tables.spotifyTokens.userId, tables.users.id) 208 ) 209 .where(eq(tables.users.did, did)) 210 .execute() ··· 214 .from(tables.googleDriveAccounts) 215 .leftJoin( 216 tables.users, 217 - eq(tables.googleDriveAccounts.userId, tables.users.id) 218 ) 219 .where(eq(tables.users.did, did)) 220 .execute() ··· 224 .from(tables.dropboxAccounts) 225 .leftJoin( 226 tables.users, 227 - eq(tables.dropboxAccounts.userId, tables.users.id) 228 ) 229 .where(eq(tables.users.did, did)) 230 .execute() ··· 280 xata_createdat: profile.user.createdAt.toISOString(), 281 xata_updatedat: profile.user.updatedAt.toISOString(), 282 xata_version: 1, 283 - }) 284 - ) 285 ); 286 } else { 287 // Update existing user in background if handle or avatar or displayName changed ··· 314 xata_createdat: profile.user.createdAt.toISOString(), 315 xata_updatedat: new Date().toISOString(), 316 xata_version: (profile.user.xataVersion || 1) + 1, 317 - }) 318 - ) 319 ); 320 } 321 }
··· 33 Effect.catchAll((err) => { 34 console.error(err); 35 return Effect.succeed({}); 36 + }), 37 ); 38 server.app.rocksky.actor.getProfile({ 39 auth: ctx.authVerifier, ··· 194 .from(tables.spotifyAccounts) 195 .leftJoin( 196 tables.users, 197 + eq(tables.spotifyAccounts.userId, tables.users.id), 198 ) 199 .where(eq(tables.users.did, did)) 200 .execute() ··· 204 .from(tables.spotifyTokens) 205 .leftJoin( 206 tables.users, 207 + eq(tables.spotifyTokens.userId, tables.users.id), 208 ) 209 .where(eq(tables.users.did, did)) 210 .execute() ··· 214 .from(tables.googleDriveAccounts) 215 .leftJoin( 216 tables.users, 217 + eq(tables.googleDriveAccounts.userId, tables.users.id), 218 ) 219 .where(eq(tables.users.did, did)) 220 .execute() ··· 224 .from(tables.dropboxAccounts) 225 .leftJoin( 226 tables.users, 227 + eq(tables.dropboxAccounts.userId, tables.users.id), 228 ) 229 .where(eq(tables.users.did, did)) 230 .execute() ··· 280 xata_createdat: profile.user.createdAt.toISOString(), 281 xata_updatedat: profile.user.updatedAt.toISOString(), 282 xata_version: 1, 283 + }), 284 + ), 285 ); 286 } else { 287 // Update existing user in background if handle or avatar or displayName changed ··· 314 xata_createdat: profile.user.createdAt.toISOString(), 315 xata_updatedat: new Date().toISOString(), 316 xata_version: (profile.user.xataVersion || 1) + 1, 317 + }), 318 + ), 319 ); 320 } 321 }
+1
apps/api/src/xrpc/app/rocksky/artist/getArtists.ts
··· 46 skip: params.offset || 0, 47 take: params.limit || 100, 48 }, 49 }); 50 return { data: response.data, ctx }; 51 },
··· 46 skip: params.offset || 0, 47 take: params.limit || 100, 48 }, 49 + names: params.names, 50 }); 51 return { data: response.data, ctx }; 52 },
+4 -4
apps/api/src/xrpc/app/rocksky/scrobble/getScrobble.ts
··· 6 import type { QueryParams } from "lexicon/types/app/rocksky/scrobble/getScrobble"; 7 import * as R from "ramda"; 8 import tables from "schema"; 9 - import { SelectAlbum } from "schema/albums"; 10 import type { SelectScrobble } from "schema/scrobbles"; 11 import type { SelectTrack } from "schema/tracks"; 12 import type { SelectUser } from "schema/users"; ··· 22 Effect.catchAll((err) => { 23 console.error("Error retrieving scrobble:", err); 24 return Effect.succeed({}); 25 - }) 26 ); 27 server.app.rocksky.scrobble.getScrobble({ 28 handler: async ({ params }) => { ··· 63 .from(tables.scrobbles) 64 .leftJoin( 65 tables.tracks, 66 - eq(tables.tracks.id, tables.scrobbles.trackId) 67 ) 68 .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)) 69 .where(eq(tables.scrobbles.trackId, scrobble?.tracks.id)) ··· 75 .from(tables.scrobbles) 76 .leftJoin( 77 tables.tracks, 78 - eq(tables.scrobbles.trackId, tables.tracks.id) 79 ) 80 .where(eq(tables.scrobbles.trackId, scrobble?.tracks.id)) 81 .execute()
··· 6 import type { QueryParams } from "lexicon/types/app/rocksky/scrobble/getScrobble"; 7 import * as R from "ramda"; 8 import tables from "schema"; 9 + import type { SelectAlbum } from "schema/albums"; 10 import type { SelectScrobble } from "schema/scrobbles"; 11 import type { SelectTrack } from "schema/tracks"; 12 import type { SelectUser } from "schema/users"; ··· 22 Effect.catchAll((err) => { 23 console.error("Error retrieving scrobble:", err); 24 return Effect.succeed({}); 25 + }), 26 ); 27 server.app.rocksky.scrobble.getScrobble({ 28 handler: async ({ params }) => { ··· 63 .from(tables.scrobbles) 64 .leftJoin( 65 tables.tracks, 66 + eq(tables.tracks.id, tables.scrobbles.trackId), 67 ) 68 .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)) 69 .where(eq(tables.scrobbles.trackId, scrobble?.tracks.id)) ··· 75 .from(tables.scrobbles) 76 .leftJoin( 77 tables.tracks, 78 + eq(tables.scrobbles.trackId, tables.tracks.id), 79 ) 80 .where(eq(tables.scrobbles.trackId, scrobble?.tracks.id)) 81 .execute()
+7 -4
apps/api/src/xrpc/app/rocksky/spotify/next.ts
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 - }) 27 ); 28 server.app.rocksky.spotify.next({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) 75 ) 76 .where(eq(tables.spotifyTokens.userId, user.id)) 77 .execute() 78 .then(([spotifyToken]) => [ 79 decrypt( 80 spotifyToken.spotify_tokens.refreshToken, 81 - env.SPOTIFY_ENCRYPTION_KEY 82 ), 83 decrypt( 84 spotifyToken.spotify_apps.spotifySecret, 85 - env.SPOTIFY_ENCRYPTION_KEY 86 ), 87 spotifyToken.spotify_apps.spotifyAppId, 88 ])
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 + }), 27 ); 28 server.app.rocksky.spotify.next({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 + eq( 75 + tables.spotifyTokens.spotifyAppId, 76 + tables.spotifyApps.spotifyAppId, 77 + ), 78 ) 79 .where(eq(tables.spotifyTokens.userId, user.id)) 80 .execute() 81 .then(([spotifyToken]) => [ 82 decrypt( 83 spotifyToken.spotify_tokens.refreshToken, 84 + env.SPOTIFY_ENCRYPTION_KEY, 85 ), 86 decrypt( 87 spotifyToken.spotify_apps.spotifySecret, 88 + env.SPOTIFY_ENCRYPTION_KEY, 89 ), 90 spotifyToken.spotify_apps.spotifyAppId, 91 ])
+7 -4
apps/api/src/xrpc/app/rocksky/spotify/pause.ts
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 - }) 27 ); 28 server.app.rocksky.spotify.pause({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) 75 ) 76 .where(eq(tables.spotifyTokens.userId, user.id)) 77 .execute() 78 .then(([spotifyToken]) => [ 79 decrypt( 80 spotifyToken.spotify_tokens.refreshToken, 81 - env.SPOTIFY_ENCRYPTION_KEY 82 ), 83 decrypt( 84 spotifyToken.spotify_apps.spotifySecret, 85 - env.SPOTIFY_ENCRYPTION_KEY 86 ), 87 spotifyToken.spotify_apps.spotifyAppId, 88 ])
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 + }), 27 ); 28 server.app.rocksky.spotify.pause({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 + eq( 75 + tables.spotifyTokens.spotifyAppId, 76 + tables.spotifyApps.spotifyAppId, 77 + ), 78 ) 79 .where(eq(tables.spotifyTokens.userId, user.id)) 80 .execute() 81 .then(([spotifyToken]) => [ 82 decrypt( 83 spotifyToken.spotify_tokens.refreshToken, 84 + env.SPOTIFY_ENCRYPTION_KEY, 85 ), 86 decrypt( 87 spotifyToken.spotify_apps.spotifySecret, 88 + env.SPOTIFY_ENCRYPTION_KEY, 89 ), 90 spotifyToken.spotify_apps.spotifyAppId, 91 ])
+7 -4
apps/api/src/xrpc/app/rocksky/spotify/play.ts
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 - }) 27 ); 28 server.app.rocksky.spotify.play({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) 75 ) 76 .where(eq(tables.spotifyTokens.userId, user.id)) 77 .execute() 78 .then(([spotifyToken]) => [ 79 decrypt( 80 spotifyToken.spotify_tokens.refreshToken, 81 - env.SPOTIFY_ENCRYPTION_KEY 82 ), 83 decrypt( 84 spotifyToken.spotify_apps.spotifySecret, 85 - env.SPOTIFY_ENCRYPTION_KEY 86 ), 87 spotifyToken.spotify_apps.spotifyAppId, 88 ])
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 + }), 27 ); 28 server.app.rocksky.spotify.play({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 + eq( 75 + tables.spotifyTokens.spotifyAppId, 76 + tables.spotifyApps.spotifyAppId, 77 + ), 78 ) 79 .where(eq(tables.spotifyTokens.userId, user.id)) 80 .execute() 81 .then(([spotifyToken]) => [ 82 decrypt( 83 spotifyToken.spotify_tokens.refreshToken, 84 + env.SPOTIFY_ENCRYPTION_KEY, 85 ), 86 decrypt( 87 spotifyToken.spotify_apps.spotifySecret, 88 + env.SPOTIFY_ENCRYPTION_KEY, 89 ), 90 spotifyToken.spotify_apps.spotifyAppId, 91 ])
+7 -4
apps/api/src/xrpc/app/rocksky/spotify/previous.ts
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 - }) 27 ); 28 server.app.rocksky.spotify.previous({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) 75 ) 76 .where(eq(tables.spotifyTokens.userId, user.id)) 77 .execute() 78 .then(([spotifyToken]) => [ 79 decrypt( 80 spotifyToken.spotify_tokens.refreshToken, 81 - env.SPOTIFY_ENCRYPTION_KEY 82 ), 83 decrypt( 84 spotifyToken.spotify_apps.spotifySecret, 85 - env.SPOTIFY_ENCRYPTION_KEY 86 ), 87 spotifyToken.spotify_apps.spotifyAppId, 88 ])
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 + }), 27 ); 28 server.app.rocksky.spotify.previous({ 29 auth: ctx.authVerifier, ··· 71 .from(tables.spotifyTokens) 72 .leftJoin( 73 tables.spotifyApps, 74 + eq( 75 + tables.spotifyTokens.spotifyAppId, 76 + tables.spotifyApps.spotifyAppId, 77 + ), 78 ) 79 .where(eq(tables.spotifyTokens.userId, user.id)) 80 .execute() 81 .then(([spotifyToken]) => [ 82 decrypt( 83 spotifyToken.spotify_tokens.refreshToken, 84 + env.SPOTIFY_ENCRYPTION_KEY, 85 ), 86 decrypt( 87 spotifyToken.spotify_apps.spotifySecret, 88 + env.SPOTIFY_ENCRYPTION_KEY, 89 ), 90 spotifyToken.spotify_apps.spotifyAppId, 91 ])
+8 -5
apps/api/src/xrpc/app/rocksky/spotify/seek.ts
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 - }) 27 ); 28 server.app.rocksky.spotify.seek({ 29 auth: ctx.authVerifier, ··· 74 .from(tables.spotifyTokens) 75 .leftJoin( 76 tables.spotifyApps, 77 - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) 78 ) 79 .where(eq(tables.spotifyTokens.userId, user.id)) 80 .execute() 81 .then(([spotifyToken]) => [ 82 decrypt( 83 spotifyToken.spotify_tokens.refreshToken, 84 - env.SPOTIFY_ENCRYPTION_KEY 85 ), 86 decrypt( 87 spotifyToken.spotify_apps.spotifySecret, 88 - env.SPOTIFY_ENCRYPTION_KEY 89 ), 90 spotifyToken.spotify_apps.spotifyAppId, 91 ]) ··· 150 headers: { 151 Authorization: `Bearer ${accessToken}`, 152 }, 153 - } 154 ).then((res) => res.status), 155 catch: (error) => new Error(`Failed to handle next action: ${error}`), 156 });
··· 23 Effect.catchAll((err) => { 24 console.error(err); 25 return Effect.succeed({}); 26 + }), 27 ); 28 server.app.rocksky.spotify.seek({ 29 auth: ctx.authVerifier, ··· 74 .from(tables.spotifyTokens) 75 .leftJoin( 76 tables.spotifyApps, 77 + eq( 78 + tables.spotifyTokens.spotifyAppId, 79 + tables.spotifyApps.spotifyAppId, 80 + ), 81 ) 82 .where(eq(tables.spotifyTokens.userId, user.id)) 83 .execute() 84 .then(([spotifyToken]) => [ 85 decrypt( 86 spotifyToken.spotify_tokens.refreshToken, 87 + env.SPOTIFY_ENCRYPTION_KEY, 88 ), 89 decrypt( 90 spotifyToken.spotify_apps.spotifySecret, 91 + env.SPOTIFY_ENCRYPTION_KEY, 92 ), 93 spotifyToken.spotify_apps.spotifyAppId, 94 ]) ··· 153 headers: { 154 Authorization: `Bearer ${accessToken}`, 155 }, 156 + }, 157 ).then((res) => res.status), 158 catch: (error) => new Error(`Failed to handle next action: ${error}`), 159 });
+124 -77
crates/analytics/src/handlers/artists.rs
··· 10 }; 11 use actix_web::{web, HttpRequest, HttpResponse}; 12 use anyhow::Error; 13 - use duckdb::Connection; 14 use tokio_stream::StreamExt; 15 16 use crate::read_payload; ··· 26 let offset = pagination.skip.unwrap_or(0); 27 let limit = pagination.take.unwrap_or(20); 28 let did = params.user_did; 29 30 let conn = conn.lock().unwrap(); 31 - let mut stmt = match did { 32 - Some(_) => conn.prepare( 33 - r#" 34 - SELECT a.*, 35 - COUNT(*) AS play_count, 36 - COUNT(DISTINCT s.user_id) AS unique_listeners 37 - FROM user_artists ua 38 - LEFT JOIN artists a ON ua.artist_id = a.id 39 - LEFT JOIN users u ON ua.user_id = u.id 40 - LEFT JOIN scrobbles s ON s.artist_id = a.id 41 - WHERE u.did = ? OR u.handle = ? 42 - GROUP BY a.* 43 - ORDER BY play_count DESC OFFSET ? LIMIT ?; 44 - "#, 45 - )?, 46 - None => conn.prepare( 47 - "SELECT a.*, 48 - COUNT(*) AS play_count, 49 - COUNT(DISTINCT s.user_id) AS unique_listeners 50 - FROM artists a 51 - LEFT JOIN scrobbles s ON s.artist_id = a.id 52 - GROUP BY a.* 53 - ORDER BY play_count DESC OFFSET ? LIMIT ?", 54 - )?, 55 - }; 56 57 - match did { 58 - Some(did) => { 59 - let artists = stmt.query_map( 60 - [&did, &did, &limit.to_string(), &offset.to_string()], 61 - |row| { 62 - Ok(Artist { 63 - id: row.get(0)?, 64 - name: row.get(1)?, 65 - biography: row.get(2)?, 66 - born: row.get(3)?, 67 - born_in: row.get(4)?, 68 - died: row.get(5)?, 69 - picture: row.get(6)?, 70 - sha256: row.get(7)?, 71 - spotify_link: row.get(8)?, 72 - tidal_link: row.get(9)?, 73 - youtube_link: row.get(10)?, 74 - apple_music_link: row.get(11)?, 75 - uri: row.get(12)?, 76 - play_count: row.get(13)?, 77 - unique_listeners: row.get(14)?, 78 - }) 79 - }, 80 - )?; 81 82 - let artists: Result<Vec<_>, _> = artists.collect(); 83 - Ok(HttpResponse::Ok().json(artists?)) 84 - } 85 - None => { 86 - let artists = stmt.query_map([limit, offset], |row| { 87 - Ok(Artist { 88 - id: row.get(0)?, 89 - name: row.get(1)?, 90 - biography: row.get(2)?, 91 - born: row.get(3)?, 92 - born_in: row.get(4)?, 93 - died: row.get(5)?, 94 - picture: row.get(6)?, 95 - sha256: row.get(7)?, 96 - spotify_link: row.get(8)?, 97 - tidal_link: row.get(9)?, 98 - youtube_link: row.get(10)?, 99 - apple_music_link: row.get(11)?, 100 - uri: row.get(12)?, 101 - play_count: row.get(13)?, 102 - unique_listeners: row.get(14)?, 103 - }) 104 - })?; 105 106 - let artists: Result<Vec<_>, _> = artists.collect(); 107 - Ok(HttpResponse::Ok().json(artists?)) 108 - } 109 - } 110 } 111 112 pub async fn get_top_artists(
··· 10 }; 11 use actix_web::{web, HttpRequest, HttpResponse}; 12 use anyhow::Error; 13 + use duckdb::{params_from_iter, Connection}; 14 use tokio_stream::StreamExt; 15 16 use crate::read_payload; ··· 26 let offset = pagination.skip.unwrap_or(0); 27 let limit = pagination.take.unwrap_or(20); 28 let did = params.user_did; 29 + let names = params.names; 30 31 let conn = conn.lock().unwrap(); 32 33 + // Build dynamic query and params based on filters 34 + let (query, params_vec): (String, Vec<Box<dyn duckdb::ToSql>>) = 35 + match (did.as_ref(), names.as_ref()) { 36 + // Both did and names provided 37 + (Some(d), Some(n)) if !n.is_empty() => { 38 + let placeholders = vec!["?"; n.len()].join(", "); 39 + let query = format!( 40 + r#" 41 + SELECT a.*, 42 + COUNT(*) AS play_count, 43 + COUNT(DISTINCT s.user_id) AS unique_listeners 44 + FROM user_artists ua 45 + LEFT JOIN artists a ON ua.artist_id = a.id 46 + LEFT JOIN users u ON ua.user_id = u.id 47 + LEFT JOIN scrobbles s ON s.artist_id = a.id 48 + WHERE (u.did = ? OR u.handle = ?) 49 + AND a.name IN ({}) 50 + GROUP BY a.* 51 + ORDER BY play_count DESC 52 + LIMIT ? OFFSET ? 53 + "#, 54 + placeholders 55 + ); 56 + let mut params: Vec<Box<dyn duckdb::ToSql>> = 57 + vec![Box::new(d.clone()), Box::new(d.clone())]; 58 + for name in n { 59 + params.push(Box::new(name.clone())); 60 + } 61 + params.push(Box::new(limit)); 62 + params.push(Box::new(offset)); 63 + (query, params) 64 + } 65 + // Only did provided 66 + (Some(d), _) => { 67 + let query = r#" 68 + SELECT a.*, 69 + COUNT(*) AS play_count, 70 + COUNT(DISTINCT s.user_id) AS unique_listeners 71 + FROM user_artists ua 72 + LEFT JOIN artists a ON ua.artist_id = a.id 73 + LEFT JOIN users u ON ua.user_id = u.id 74 + LEFT JOIN scrobbles s ON s.artist_id = a.id 75 + WHERE u.did = ? OR u.handle = ? 76 + GROUP BY a.* 77 + ORDER BY play_count DESC 78 + LIMIT ? OFFSET ? 79 + "# 80 + .to_string(); 81 + ( 82 + query, 83 + vec![ 84 + Box::new(d.clone()), 85 + Box::new(d.clone()), 86 + Box::new(limit), 87 + Box::new(offset), 88 + ], 89 + ) 90 + } 91 + // Only names provided 92 + (None, Some(n)) if !n.is_empty() => { 93 + let placeholders = vec!["?"; n.len()].join(", "); 94 + let query = format!( 95 + r#" 96 + SELECT a.*, 97 + COUNT(*) AS play_count, 98 + COUNT(DISTINCT s.user_id) AS unique_listeners 99 + FROM artists a 100 + LEFT JOIN scrobbles s ON s.artist_id = a.id 101 + WHERE a.name IN ({}) 102 + GROUP BY a.* 103 + ORDER BY play_count DESC 104 + LIMIT ? OFFSET ? 105 + "#, 106 + placeholders 107 + ); 108 + let mut params: Vec<Box<dyn duckdb::ToSql>> = vec![]; 109 + for name in n { 110 + params.push(Box::new(name.clone())); 111 + } 112 + params.push(Box::new(limit)); 113 + params.push(Box::new(offset)); 114 + (query, params) 115 + } 116 + // No filters 117 + (None, _) => { 118 + let query = r#" 119 + SELECT a.*, 120 + COUNT(*) AS play_count, 121 + COUNT(DISTINCT s.user_id) AS unique_listeners 122 + FROM artists a 123 + LEFT JOIN scrobbles s ON s.artist_id = a.id 124 + GROUP BY a.* 125 + ORDER BY play_count DESC 126 + LIMIT ? OFFSET ? 127 + "# 128 + .to_string(); 129 + (query, vec![Box::new(limit), Box::new(offset)]) 130 + } 131 + }; 132 133 + // Prepare and execute query 134 + let mut stmt = conn.prepare(&query)?; 135 + let artists = stmt.query_map(params_from_iter(params_vec.iter()), |row| { 136 + Ok(Artist { 137 + id: row.get(0)?, 138 + name: row.get(1)?, 139 + biography: row.get(2)?, 140 + born: row.get(3)?, 141 + born_in: row.get(4)?, 142 + died: row.get(5)?, 143 + picture: row.get(6)?, 144 + sha256: row.get(7)?, 145 + spotify_link: row.get(8)?, 146 + tidal_link: row.get(9)?, 147 + youtube_link: row.get(10)?, 148 + apple_music_link: row.get(11)?, 149 + uri: row.get(12)?, 150 + play_count: row.get(14)?, 151 + unique_listeners: row.get(15)?, 152 + }) 153 + })?; 154 155 + let artists: Result<Vec<_>, _> = artists.collect(); 156 + Ok(HttpResponse::Ok().json(artists?)) 157 } 158 159 pub async fn get_top_artists(
+1
crates/analytics/src/types/artist.rs
··· 60 pub struct GetArtistsParams { 61 pub user_did: Option<String>, 62 pub pagination: Option<Pagination>, 63 } 64 65 #[derive(Debug, Serialize, Deserialize, Default)]
··· 60 pub struct GetArtistsParams { 61 pub user_did: Option<String>, 62 pub pagination: Option<Pagination>, 63 + pub names: Option<Vec<String>>, 64 } 65 66 #[derive(Debug, Serialize, Deserialize, Default)]