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

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