From 485c1e32eefe94daf2b843da1aea8bdce855b865 Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Fri, 19 Dec 2025 08:15:19 +0300 Subject: [PATCH] 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. --- apps/api/lexicons/artist/getArtists.json | 4 + apps/api/pkl/defs/artist/getArtists.pkl | 4 + apps/api/src/auth/client.ts | 2 +- apps/api/src/auth/storage.ts | 2 +- apps/api/src/bsky/app.ts | 21 +- apps/api/src/db.ts | 6 +- apps/api/src/index.ts | 6 +- apps/api/src/lexicon/index.ts | 78 +++---- apps/api/src/lexicon/lexicons.ts | 170 +++++++-------- .../types/app/rocksky/artist/getArtists.ts | 2 + apps/api/src/nowplaying/nowplaying.service.ts | 92 ++++---- apps/api/src/schema/spotify-accounts.ts | 4 +- apps/api/src/schema/spotify-apps.ts | 4 +- apps/api/src/schema/spotify-tokens.ts | 4 +- apps/api/src/scripts/genres.ts | 12 +- apps/api/src/scripts/spotify.ts | 4 +- apps/api/src/scripts/sync.ts | 2 +- apps/api/src/server.ts | 2 +- apps/api/src/spotify/app.ts | 60 +++--- apps/api/src/tealfm/index.ts | 14 +- .../src/xrpc/app/rocksky/actor/getProfile.ts | 18 +- .../src/xrpc/app/rocksky/artist/getArtists.ts | 1 + .../xrpc/app/rocksky/scrobble/getScrobble.ts | 8 +- apps/api/src/xrpc/app/rocksky/spotify/next.ts | 11 +- .../api/src/xrpc/app/rocksky/spotify/pause.ts | 11 +- apps/api/src/xrpc/app/rocksky/spotify/play.ts | 11 +- .../src/xrpc/app/rocksky/spotify/previous.ts | 11 +- apps/api/src/xrpc/app/rocksky/spotify/seek.ts | 13 +- crates/analytics/src/handlers/artists.rs | 201 +++++++++++------- crates/analytics/src/types/artist.rs | 1 + 30 files changed, 426 insertions(+), 353 deletions(-) diff --git a/apps/api/lexicons/artist/getArtists.json b/apps/api/lexicons/artist/getArtists.json index f579cc37..da7db6d7 100644 --- a/apps/api/lexicons/artist/getArtists.json +++ b/apps/api/lexicons/artist/getArtists.json @@ -17,6 +17,10 @@ "type": "integer", "description": "The offset for pagination", "minimum": 0 + }, + "names": { + "type": "string", + "description": "The names of the artists to return" } } }, diff --git a/apps/api/pkl/defs/artist/getArtists.pkl b/apps/api/pkl/defs/artist/getArtists.pkl index 3c8961b9..d224dbe1 100644 --- a/apps/api/pkl/defs/artist/getArtists.pkl +++ b/apps/api/pkl/defs/artist/getArtists.pkl @@ -18,6 +18,10 @@ defs = new Mapping { description = "The offset for pagination" minimum = 0 } + ["names"] = new StringType { + type = "string" + description = "The names of the artists to return" + } } } output { diff --git a/apps/api/src/auth/client.ts b/apps/api/src/auth/client.ts index 39970fea..34918120 100644 --- a/apps/api/src/auth/client.ts +++ b/apps/api/src/auth/client.ts @@ -29,7 +29,7 @@ export const createClient = async (db: Database) => { client_id: publicUrl ? `${url}/oauth-client-metadata.json` : `http://localhost?redirect_uri=${enc( - `${url}/oauth/callback` + `${url}/oauth/callback`, )}&scope=${enc("atproto transition:generic")}`, client_uri: url, redirect_uris: [`${url}/oauth/callback`], diff --git a/apps/api/src/auth/storage.ts b/apps/api/src/auth/storage.ts index 137ce193..367ad8f3 100644 --- a/apps/api/src/auth/storage.ts +++ b/apps/api/src/auth/storage.ts @@ -47,7 +47,7 @@ export class SessionStore implements NodeSavedSessionStore { .insertInto("auth_session") .values({ key, session, expiresAt: val.tokenSet.expires_at }) .onConflict((oc) => - oc.doUpdateSet({ session, expiresAt: val.tokenSet.expires_at }) + oc.doUpdateSet({ session, expiresAt: val.tokenSet.expires_at }), ) .execute(); } diff --git a/apps/api/src/bsky/app.ts b/apps/api/src/bsky/app.ts index bdba1cfa..3df36588 100644 --- a/apps/api/src/bsky/app.ts +++ b/apps/api/src/bsky/app.ts @@ -81,7 +81,7 @@ app.get("/oauth/callback", async (c) => { ? Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 1000 : Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, }, - env.JWT_SECRET + env.JWT_SECRET, ); ctx.kv.set(did, token); } catch (err) { @@ -93,7 +93,10 @@ app.get("/oauth/callback", async (c) => { .select() .from(spotifyAccounts) .where( - and(eq(spotifyAccounts.userId, did), eq(spotifyAccounts.isBetaUser, true)) + and( + eq(spotifyAccounts.userId, did), + eq(spotifyAccounts.isBetaUser, true), + ), ) .limit(1) .execute(); @@ -179,7 +182,7 @@ app.get("/profile", async (c) => { ctx.nc.publish( "rocksky.user", - Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))) + Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))), ); await ctx.kv.set("lastUser", lastUser[0].id); @@ -192,8 +195,8 @@ app.get("/profile", async (c) => { .where( and( eq(spotifyAccounts.userId, did), - eq(spotifyAccounts.isBetaUser, true) - ) + eq(spotifyAccounts.isBetaUser, true), + ), ) .limit(1) .execute(), @@ -209,8 +212,8 @@ app.get("/profile", async (c) => { .where( and( eq(googleDriveAccounts.userId, did), - eq(googleDriveAccounts.isBetaUser, true) - ) + eq(googleDriveAccounts.isBetaUser, true), + ), ) .limit(1) .execute(), @@ -220,8 +223,8 @@ app.get("/profile", async (c) => { .where( and( eq(dropboxAccounts.userId, did), - eq(dropboxAccounts.isBetaUser, true) - ) + eq(dropboxAccounts.isBetaUser, true), + ), ) .limit(1) .execute(), diff --git a/apps/api/src/db.ts b/apps/api/src/db.ts index 48a35c38..4d091b23 100644 --- a/apps/api/src/db.ts +++ b/apps/api/src/db.ts @@ -131,7 +131,7 @@ export const updateExpiresAt = async (db: Database) => { export const refreshSessionsAboutToExpire = async ( db: Database, - ctx: Context + ctx: Context, ) => { const now = new Date().toISOString(); @@ -147,7 +147,7 @@ export const refreshSessionsAboutToExpire = async ( console.log( "Session about to expire:", chalk.cyan(session.key), - session.expiresAt + session.expiresAt, ); const agent = await createAgent(ctx.oauthClient, session.key); // Trigger a token refresh by fetching preferences @@ -156,7 +156,7 @@ export const refreshSessionsAboutToExpire = async ( } console.log( - `Found ${chalk.yellowBright(sessions.length)} sessions to refresh` + `Found ${chalk.yellowBright(sessions.length)} sessions to refresh`, ); }; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 9d29d1b4..10324f1a 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -50,7 +50,7 @@ app.use( rateLimiter({ limit: 1000, window: 30, // 👈 30 seconds - }) + }), ); app.use("*", async (c, next) => { @@ -165,7 +165,7 @@ app.get("/now-playing", async (c) => { ctx.redis.get(`nowplaying:${user.did}:status`), ]); return c.json( - nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {} + nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {}, ); }); @@ -317,7 +317,7 @@ app.get("/public/scrobbles", async (c) => { listeners: 1, sha256: item.track.sha256, id: item.scrobble.id, - })) + })), ); }); diff --git a/apps/api/src/lexicon/index.ts b/apps/api/src/lexicon/index.ts index 5df583e2..e7a95afe 100644 --- a/apps/api/src/lexicon/index.ts +++ b/apps/api/src/lexicon/index.ts @@ -22,17 +22,17 @@ import type * as AppRockskyActorGetActorScrobbles from "./types/app/rocksky/acto import type * as AppRockskyActorGetActorSongs from "./types/app/rocksky/actor/getActorSongs"; import type * as AppRockskyActorGetProfile from "./types/app/rocksky/actor/getProfile"; import type * as AppRockskyAlbumGetAlbum from "./types/app/rocksky/album/getAlbum"; -import type * as AppRockskyAlbumGetAlbums from "./types/app/rocksky/album/getAlbums"; import type * as AppRockskyAlbumGetAlbumTracks from "./types/app/rocksky/album/getAlbumTracks"; +import type * as AppRockskyAlbumGetAlbums from "./types/app/rocksky/album/getAlbums"; import type * as AppRockskyApikeyCreateApikey from "./types/app/rocksky/apikey/createApikey"; import type * as AppRockskyApikeyGetApikeys from "./types/app/rocksky/apikey/getApikeys"; import type * as AppRockskyApikeyRemoveApikey from "./types/app/rocksky/apikey/removeApikey"; import type * as AppRockskyApikeyUpdateApikey from "./types/app/rocksky/apikey/updateApikey"; -import type * as AppRockskyArtistGetArtistAlbums from "./types/app/rocksky/artist/getArtistAlbums"; import type * as AppRockskyArtistGetArtist from "./types/app/rocksky/artist/getArtist"; +import type * as AppRockskyArtistGetArtistAlbums from "./types/app/rocksky/artist/getArtistAlbums"; import type * as AppRockskyArtistGetArtistListeners from "./types/app/rocksky/artist/getArtistListeners"; -import type * as AppRockskyArtistGetArtists from "./types/app/rocksky/artist/getArtists"; import type * as AppRockskyArtistGetArtistTracks from "./types/app/rocksky/artist/getArtistTracks"; +import type * as AppRockskyArtistGetArtists from "./types/app/rocksky/artist/getArtists"; import type * as AppRockskyChartsGetScrobblesChart from "./types/app/rocksky/charts/getScrobblesChart"; import type * as AppRockskyDropboxDownloadFile from "./types/app/rocksky/dropbox/downloadFile"; import type * as AppRockskyDropboxGetFiles from "./types/app/rocksky/dropbox/getFiles"; @@ -53,9 +53,9 @@ import type * as AppRockskyPlayerGetCurrentlyPlaying from "./types/app/rocksky/p import type * as AppRockskyPlayerGetPlaybackQueue from "./types/app/rocksky/player/getPlaybackQueue"; import type * as AppRockskyPlayerNext from "./types/app/rocksky/player/next"; import type * as AppRockskyPlayerPause from "./types/app/rocksky/player/pause"; +import type * as AppRockskyPlayerPlay from "./types/app/rocksky/player/play"; import type * as AppRockskyPlayerPlayDirectory from "./types/app/rocksky/player/playDirectory"; import type * as AppRockskyPlayerPlayFile from "./types/app/rocksky/player/playFile"; -import type * as AppRockskyPlayerPlay from "./types/app/rocksky/player/play"; import type * as AppRockskyPlayerPrevious from "./types/app/rocksky/player/previous"; import type * as AppRockskyPlayerSeek from "./types/app/rocksky/player/seek"; import type * as AppRockskyPlaylistCreatePlaylist from "./types/app/rocksky/playlist/createPlaylist"; @@ -365,25 +365,25 @@ export class AppRockskyAlbumNS { return this._server.xrpc.method(nsid, cfg); } - getAlbums( + getAlbumTracks( cfg: ConfigOf< AV, - AppRockskyAlbumGetAlbums.Handler>, - AppRockskyAlbumGetAlbums.HandlerReqCtx> + AppRockskyAlbumGetAlbumTracks.Handler>, + AppRockskyAlbumGetAlbumTracks.HandlerReqCtx> >, ) { - const nsid = "app.rocksky.album.getAlbums"; // @ts-ignore + const nsid = "app.rocksky.album.getAlbumTracks"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } - getAlbumTracks( + getAlbums( cfg: ConfigOf< AV, - AppRockskyAlbumGetAlbumTracks.Handler>, - AppRockskyAlbumGetAlbumTracks.HandlerReqCtx> + AppRockskyAlbumGetAlbums.Handler>, + AppRockskyAlbumGetAlbums.HandlerReqCtx> >, ) { - const nsid = "app.rocksky.album.getAlbumTracks"; // @ts-ignore + const nsid = "app.rocksky.album.getAlbums"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } } @@ -447,25 +447,25 @@ export class AppRockskyArtistNS { this._server = server; } - getArtistAlbums( + getArtist( cfg: ConfigOf< AV, - AppRockskyArtistGetArtistAlbums.Handler>, - AppRockskyArtistGetArtistAlbums.HandlerReqCtx> + AppRockskyArtistGetArtist.Handler>, + AppRockskyArtistGetArtist.HandlerReqCtx> >, ) { - const nsid = "app.rocksky.artist.getArtistAlbums"; // @ts-ignore + const nsid = "app.rocksky.artist.getArtist"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } - getArtist( + getArtistAlbums( cfg: ConfigOf< AV, - AppRockskyArtistGetArtist.Handler>, - AppRockskyArtistGetArtist.HandlerReqCtx> + AppRockskyArtistGetArtistAlbums.Handler>, + AppRockskyArtistGetArtistAlbums.HandlerReqCtx> >, ) { - const nsid = "app.rocksky.artist.getArtist"; // @ts-ignore + const nsid = "app.rocksky.artist.getArtistAlbums"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } @@ -480,25 +480,25 @@ export class AppRockskyArtistNS { return this._server.xrpc.method(nsid, cfg); } - getArtists( + getArtistTracks( cfg: ConfigOf< AV, - AppRockskyArtistGetArtists.Handler>, - AppRockskyArtistGetArtists.HandlerReqCtx> + AppRockskyArtistGetArtistTracks.Handler>, + AppRockskyArtistGetArtistTracks.HandlerReqCtx> >, ) { - const nsid = "app.rocksky.artist.getArtists"; // @ts-ignore + const nsid = "app.rocksky.artist.getArtistTracks"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } - getArtistTracks( + getArtists( cfg: ConfigOf< AV, - AppRockskyArtistGetArtistTracks.Handler>, - AppRockskyArtistGetArtistTracks.HandlerReqCtx> + AppRockskyArtistGetArtists.Handler>, + AppRockskyArtistGetArtists.HandlerReqCtx> >, ) { - const nsid = "app.rocksky.artist.getArtistTracks"; // @ts-ignore + const nsid = "app.rocksky.artist.getArtists"; // @ts-ignore return this._server.xrpc.method(nsid, cfg); } } @@ -770,6 +770,17 @@ export class AppRockskyPlayerNS { return this._server.xrpc.method(nsid, cfg); } + play( + cfg: ConfigOf< + AV, + AppRockskyPlayerPlay.Handler>, + AppRockskyPlayerPlay.HandlerReqCtx> + >, + ) { + const nsid = "app.rocksky.player.play"; // @ts-ignore + return this._server.xrpc.method(nsid, cfg); + } + playDirectory( cfg: ConfigOf< AV, @@ -792,17 +803,6 @@ export class AppRockskyPlayerNS { return this._server.xrpc.method(nsid, cfg); } - play( - cfg: ConfigOf< - AV, - AppRockskyPlayerPlay.Handler>, - AppRockskyPlayerPlay.HandlerReqCtx> - >, - ) { - const nsid = "app.rocksky.player.play"; // @ts-ignore - return this._server.xrpc.method(nsid, cfg); - } - previous( cfg: ConfigOf< AV, diff --git a/apps/api/src/lexicon/lexicons.ts b/apps/api/src/lexicon/lexicons.ts index d0ef883b..36491ada 100644 --- a/apps/api/src/lexicon/lexicons.ts +++ b/apps/api/src/lexicon/lexicons.ts @@ -1267,25 +1267,21 @@ export const schemaDict = { }, }, }, - AppRockskyAlbumGetAlbums: { + AppRockskyAlbumGetAlbumTracks: { lexicon: 1, - id: "app.rocksky.album.getAlbums", + id: "app.rocksky.album.getAlbumTracks", defs: { main: { type: "query", - description: "Get albums", + description: "Get tracks for an album", parameters: { type: "params", + required: ["uri"], properties: { - limit: { - type: "integer", - description: "The maximum number of albums to return", - minimum: 1, - }, - offset: { - type: "integer", - description: "The offset for pagination", - minimum: 0, + uri: { + type: "string", + description: "The URI of the album to retrieve tracks from", + format: "at-uri", }, }, }, @@ -1294,11 +1290,11 @@ export const schemaDict = { schema: { type: "object", properties: { - albums: { + tracks: { type: "array", items: { type: "ref", - ref: "lex:app.rocksky.album.defs#albumViewBasic", + ref: "lex:app.rocksky.song.defs#songViewBasic", }, }, }, @@ -1307,21 +1303,25 @@ export const schemaDict = { }, }, }, - AppRockskyAlbumGetAlbumTracks: { + AppRockskyAlbumGetAlbums: { lexicon: 1, - id: "app.rocksky.album.getAlbumTracks", + id: "app.rocksky.album.getAlbums", defs: { main: { type: "query", - description: "Get tracks for an album", + description: "Get albums", parameters: { type: "params", - required: ["uri"], properties: { - uri: { - type: "string", - description: "The URI of the album to retrieve tracks from", - format: "at-uri", + limit: { + type: "integer", + description: "The maximum number of albums to return", + minimum: 1, + }, + offset: { + type: "integer", + description: "The offset for pagination", + minimum: 0, }, }, }, @@ -1330,11 +1330,11 @@ export const schemaDict = { schema: { type: "object", properties: { - tracks: { + albums: { type: "array", items: { type: "ref", - ref: "lex:app.rocksky.song.defs#songViewBasic", + ref: "lex:app.rocksky.album.defs#albumViewBasic", }, }, }, @@ -1737,20 +1737,20 @@ export const schemaDict = { }, }, }, - AppRockskyArtistGetArtistAlbums: { + AppRockskyArtistGetArtist: { lexicon: 1, - id: "app.rocksky.artist.getArtistAlbums", + id: "app.rocksky.artist.getArtist", defs: { main: { type: "query", - description: "Get artist's albums", + description: "Get artist details", parameters: { type: "params", required: ["uri"], properties: { uri: { type: "string", - description: "The URI of the artist to retrieve albums from", + description: "The URI of the artist to retrieve details from", format: "at-uri", }, }, @@ -1758,35 +1758,27 @@ export const schemaDict = { output: { encoding: "application/json", schema: { - type: "object", - properties: { - albums: { - type: "array", - items: { - type: "ref", - ref: "lex:app.rocksky.album.defs#albumViewBasic", - }, - }, - }, + type: "ref", + ref: "lex:app.rocksky.artist.defs#artistViewDetailed", }, }, }, }, }, - AppRockskyArtistGetArtist: { + AppRockskyArtistGetArtistAlbums: { lexicon: 1, - id: "app.rocksky.artist.getArtist", + id: "app.rocksky.artist.getArtistAlbums", defs: { main: { type: "query", - description: "Get artist details", + description: "Get artist's albums", parameters: { type: "params", required: ["uri"], properties: { uri: { type: "string", - description: "The URI of the artist to retrieve details from", + description: "The URI of the artist to retrieve albums from", format: "at-uri", }, }, @@ -1794,8 +1786,16 @@ export const schemaDict = { output: { encoding: "application/json", schema: { - type: "ref", - ref: "lex:app.rocksky.artist.defs#artistViewDetailed", + type: "object", + properties: { + albums: { + type: "array", + items: { + type: "ref", + ref: "lex:app.rocksky.album.defs#albumViewBasic", + }, + }, + }, }, }, }, @@ -1845,19 +1845,24 @@ export const schemaDict = { }, }, }, - AppRockskyArtistGetArtists: { + AppRockskyArtistGetArtistTracks: { lexicon: 1, - id: "app.rocksky.artist.getArtists", + id: "app.rocksky.artist.getArtistTracks", defs: { main: { type: "query", - description: "Get artists", + description: "Get artist's tracks", parameters: { type: "params", properties: { + uri: { + type: "string", + description: "The URI of the artist to retrieve albums from", + format: "at-uri", + }, limit: { type: "integer", - description: "The maximum number of artists to return", + description: "The maximum number of tracks to return", minimum: 1, }, offset: { @@ -1872,11 +1877,11 @@ export const schemaDict = { schema: { type: "object", properties: { - artists: { + tracks: { type: "array", items: { type: "ref", - ref: "lex:app.rocksky.artist.defs#artistViewBasic", + ref: "lex:app.rocksky.song.defs#songViewBasic", }, }, }, @@ -1885,24 +1890,19 @@ export const schemaDict = { }, }, }, - AppRockskyArtistGetArtistTracks: { + AppRockskyArtistGetArtists: { lexicon: 1, - id: "app.rocksky.artist.getArtistTracks", + id: "app.rocksky.artist.getArtists", defs: { main: { type: "query", - description: "Get artist's tracks", + description: "Get artists", parameters: { type: "params", properties: { - uri: { - type: "string", - description: "The URI of the artist to retrieve albums from", - format: "at-uri", - }, limit: { type: "integer", - description: "The maximum number of tracks to return", + description: "The maximum number of artists to return", minimum: 1, }, offset: { @@ -1910,6 +1910,10 @@ export const schemaDict = { description: "The offset for pagination", minimum: 0, }, + names: { + type: "string", + description: "The names of the artists to return", + }, }, }, output: { @@ -1917,11 +1921,11 @@ export const schemaDict = { schema: { type: "object", properties: { - tracks: { + artists: { type: "array", items: { type: "ref", - ref: "lex:app.rocksky.song.defs#songViewBasic", + ref: "lex:app.rocksky.artist.defs#artistViewBasic", }, }, }, @@ -2766,6 +2770,24 @@ export const schemaDict = { }, }, }, + AppRockskyPlayerPlay: { + lexicon: 1, + id: "app.rocksky.player.play", + defs: { + main: { + type: "procedure", + description: "Resume playback of the currently paused track", + parameters: { + type: "params", + properties: { + playerId: { + type: "string", + }, + }, + }, + }, + }, + }, AppRockskyPlayerPlayDirectory: { lexicon: 1, id: "app.rocksky.player.playDirectory", @@ -2819,24 +2841,6 @@ export const schemaDict = { }, }, }, - AppRockskyPlayerPlay: { - lexicon: 1, - id: "app.rocksky.player.play", - defs: { - main: { - type: "procedure", - description: "Resume playback of the currently paused track", - parameters: { - type: "params", - properties: { - playerId: { - type: "string", - }, - }, - }, - }, - }, - }, AppRockskyPlayerPrevious: { lexicon: 1, id: "app.rocksky.player.previous", @@ -5058,8 +5062,8 @@ export const ids = { AppRockskyAlbum: "app.rocksky.album", AppRockskyAlbumDefs: "app.rocksky.album.defs", AppRockskyAlbumGetAlbum: "app.rocksky.album.getAlbum", - AppRockskyAlbumGetAlbums: "app.rocksky.album.getAlbums", AppRockskyAlbumGetAlbumTracks: "app.rocksky.album.getAlbumTracks", + AppRockskyAlbumGetAlbums: "app.rocksky.album.getAlbums", AppRockskyApikeyCreateApikey: "app.rocksky.apikey.createApikey", AppRockskyApikeyDefs: "app.rocksky.apikey.defs", AppRockskyApikeysDefs: "app.rocksky.apikeys.defs", @@ -5068,11 +5072,11 @@ export const ids = { AppRockskyApikeyUpdateApikey: "app.rocksky.apikey.updateApikey", AppRockskyArtist: "app.rocksky.artist", AppRockskyArtistDefs: "app.rocksky.artist.defs", - AppRockskyArtistGetArtistAlbums: "app.rocksky.artist.getArtistAlbums", AppRockskyArtistGetArtist: "app.rocksky.artist.getArtist", + AppRockskyArtistGetArtistAlbums: "app.rocksky.artist.getArtistAlbums", AppRockskyArtistGetArtistListeners: "app.rocksky.artist.getArtistListeners", - AppRockskyArtistGetArtists: "app.rocksky.artist.getArtists", AppRockskyArtistGetArtistTracks: "app.rocksky.artist.getArtistTracks", + AppRockskyArtistGetArtists: "app.rocksky.artist.getArtists", AppRockskyChartsDefs: "app.rocksky.charts.defs", AppRockskyChartsGetScrobblesChart: "app.rocksky.charts.getScrobblesChart", AppRockskyDropboxDefs: "app.rocksky.dropbox.defs", @@ -5099,9 +5103,9 @@ export const ids = { AppRockskyPlayerGetPlaybackQueue: "app.rocksky.player.getPlaybackQueue", AppRockskyPlayerNext: "app.rocksky.player.next", AppRockskyPlayerPause: "app.rocksky.player.pause", + AppRockskyPlayerPlay: "app.rocksky.player.play", AppRockskyPlayerPlayDirectory: "app.rocksky.player.playDirectory", AppRockskyPlayerPlayFile: "app.rocksky.player.playFile", - AppRockskyPlayerPlay: "app.rocksky.player.play", AppRockskyPlayerPrevious: "app.rocksky.player.previous", AppRockskyPlayerSeek: "app.rocksky.player.seek", AppRockskyPlaylistCreatePlaylist: "app.rocksky.playlist.createPlaylist", diff --git a/apps/api/src/lexicon/types/app/rocksky/artist/getArtists.ts b/apps/api/src/lexicon/types/app/rocksky/artist/getArtists.ts index 86c3f04d..65fc5e9f 100644 --- a/apps/api/src/lexicon/types/app/rocksky/artist/getArtists.ts +++ b/apps/api/src/lexicon/types/app/rocksky/artist/getArtists.ts @@ -14,6 +14,8 @@ export interface QueryParams { limit?: number; /** The offset for pagination */ offset?: number; + /** The names of the artists to return */ + names?: string; } export type InputSchema = undefined; diff --git a/apps/api/src/nowplaying/nowplaying.service.ts b/apps/api/src/nowplaying/nowplaying.service.ts index 4709a055..d1875e80 100644 --- a/apps/api/src/nowplaying/nowplaying.service.ts +++ b/apps/api/src/nowplaying/nowplaying.service.ts @@ -26,7 +26,7 @@ import tealfm from "../tealfm"; export async function putArtistRecord( track: Track, - agent: Agent + agent: Agent, ): Promise { const rkey = TID.nextStr(); const record: Artist.Record = { @@ -62,7 +62,7 @@ export async function putArtistRecord( export async function putAlbumRecord( track: Track, - agent: Agent + agent: Agent, ): Promise { const rkey = TID.nextStr(); @@ -103,7 +103,7 @@ export async function putAlbumRecord( export async function putSongRecord( track: Track, - agent: Agent + agent: Agent, ): Promise { const rkey = TID.nextStr(); @@ -158,7 +158,7 @@ export async function putSongRecord( async function putScrobbleRecord( track: Track, - agent: Agent + agent: Agent, ): Promise { const rkey = TID.nextStr(); @@ -276,8 +276,8 @@ export async function publishScrobble(ctx: Context, id: string) { .where( and( eq(artistAlbums.albumId, scrobble.album.id), - eq(artistAlbums.artistId, scrobble.artist.id) - ) + eq(artistAlbums.artistId, scrobble.artist.id), + ), ) .limit(1) .then((rows) => rows[0]), @@ -440,12 +440,12 @@ export async function publishScrobble(ctx: Context, id: string) { }, }), null, - 2 + 2, ); ctx.nc.publish( "rocksky.scrobble", - Buffer.from(message.replaceAll("sha_256", "sha256")) + Buffer.from(message.replaceAll("sha_256", "sha256")), ); const trackMessage = JSON.stringify( @@ -492,12 +492,12 @@ export async function publishScrobble(ctx: Context, id: string) { xata_createdat: artist_album.createdAt.toISOString(), xata_updatedat: artist_album.updatedAt.toISOString(), }, - }) + }), ); ctx.nc.publish( "rocksky.track", - Buffer.from(trackMessage.replaceAll("sha_256", "sha256")) + Buffer.from(trackMessage.replaceAll("sha_256", "sha256")), ); } @@ -505,7 +505,7 @@ export async function scrobbleTrack( ctx: Context, track: Track, agent: Agent, - userDid: string + userDid: string, ): Promise { // check if scrobble already exists (user did + timestamp) const scrobbleTime = dayjs.unix(track.timestamp || dayjs().unix()); @@ -524,8 +524,8 @@ export async function scrobbleTrack( eq(tracks.title, track.title), eq(tracks.artist, track.artist), gte(scrobbles.timestamp, scrobbleTime.subtract(60, "seconds").toDate()), - lte(scrobbles.timestamp, scrobbleTime.add(60, "seconds").toDate()) - ) + lte(scrobbles.timestamp, scrobbleTime.add(60, "seconds").toDate()), + ), ) .limit(1) .then((rows) => rows[0]); @@ -533,8 +533,8 @@ export async function scrobbleTrack( if (existingScrobble) { console.log( `Scrobble already exists for ${chalk.cyan(track.title)} at ${chalk.cyan( - scrobbleTime.format("YYYY-MM-DD HH:mm:ss") - )}` + scrobbleTime.format("YYYY-MM-DD HH:mm:ss"), + )}`, ); return; } @@ -547,10 +547,10 @@ export async function scrobbleTrack( tracks.sha256, createHash("sha256") .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), ) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -564,8 +564,8 @@ export async function scrobbleTrack( albums.sha256, createHash("sha256") .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -586,8 +586,8 @@ export async function scrobbleTrack( artists.sha256, createHash("sha256") .update(track.albumArtist.toLowerCase()) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -618,7 +618,7 @@ export async function scrobbleTrack( artist: track.artist.split(",").map((a) => ({ name: a.trim() })), name: track.title, album: track.album, - } + }, ); if (!mbTrack?.trackMBID) { @@ -647,8 +647,8 @@ export async function scrobbleTrack( albums.sha256, createHash("sha256") .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -664,10 +664,10 @@ export async function scrobbleTrack( tracks.sha256, createHash("sha256") .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), ) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -681,7 +681,7 @@ export async function scrobbleTrack( if (existingTrack) { console.log( - `Song found: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` + `Song found: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, ); } @@ -694,13 +694,13 @@ export async function scrobbleTrack( artists.sha256, createHash("sha256") .update(track.albumArtist.toLowerCase()) - .digest("hex") + .digest("hex"), ), eq( artists.sha256, - createHash("sha256").update(track.artist.toLowerCase()).digest("hex") - ) - ) + createHash("sha256").update(track.artist.toLowerCase()).digest("hex"), + ), + ), ) .limit(1) .then((rows) => rows[0]); @@ -715,7 +715,7 @@ export async function scrobbleTrack( .innerJoin(artists, eq(userArtists.artistId, artists.id)) .innerJoin(users, eq(userArtists.userId, users.id)) .where( - and(eq(artists.id, existingArtist?.id || ""), eq(users.did, userDid)) + and(eq(artists.id, existingArtist?.id || ""), eq(users.did, userDid)), ) .limit(1) .then((rows) => rows[0]); @@ -750,17 +750,17 @@ export async function scrobbleTrack( tracks.sha256, createHash("sha256") .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), ) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); while (!existingTrack?.artistUri && !existingTrack?.albumUri && tries < 30) { console.log( - `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}` + `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}`, ); existingTrack = await ctx.db .select() @@ -770,10 +770,10 @@ export async function scrobbleTrack( tracks.sha256, createHash("sha256") .update( - `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() + `${track.title} - ${track.artist} - ${track.album}`.toLowerCase(), ) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -788,8 +788,8 @@ export async function scrobbleTrack( artists.sha256, createHash("sha256") .update(track.albumArtist.toLowerCase()) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -812,8 +812,8 @@ export async function scrobbleTrack( albums.sha256, createHash("sha256") .update(`${track.album} - ${track.albumArtist}`.toLowerCase()) - .digest("hex") - ) + .digest("hex"), + ), ) .limit(1) .then((rows) => rows[0]); @@ -843,7 +843,7 @@ export async function scrobbleTrack( if (existingTrack?.artistUri) { console.log( - `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` + `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`, ); } diff --git a/apps/api/src/schema/spotify-accounts.ts b/apps/api/src/schema/spotify-accounts.ts index e885a6f8..32f2c909 100644 --- a/apps/api/src/schema/spotify-accounts.ts +++ b/apps/api/src/schema/spotify-accounts.ts @@ -9,9 +9,7 @@ import { import users from "./users"; const spotifyAccounts = pgTable("spotify_accounts", { - id: text("xata_id") - .primaryKey() - .default(sql`xata_id()`), + id: text("xata_id").primaryKey().default(sql`xata_id()`), xataVersion: integer("xata_version"), email: text("email").notNull(), userId: text("user_id") diff --git a/apps/api/src/schema/spotify-apps.ts b/apps/api/src/schema/spotify-apps.ts index c28b5bbe..9990d634 100644 --- a/apps/api/src/schema/spotify-apps.ts +++ b/apps/api/src/schema/spotify-apps.ts @@ -2,9 +2,7 @@ import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; const spotifyApps = pgTable("spotify_apps", { - id: text("xata_id") - .primaryKey() - .default(sql`xata_id()`), + id: text("xata_id").primaryKey().default(sql`xata_id()`), xataVersion: integer("xata_version"), spotifyAppId: text("spotify_app_id").unique().notNull(), spotifySecret: text("spotify_secret").notNull(), diff --git a/apps/api/src/schema/spotify-tokens.ts b/apps/api/src/schema/spotify-tokens.ts index 732cb2e1..79934c44 100644 --- a/apps/api/src/schema/spotify-tokens.ts +++ b/apps/api/src/schema/spotify-tokens.ts @@ -3,9 +3,7 @@ import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; import users from "./users"; const spotifyTokens = pgTable("spotify_tokens", { - id: text("xata_id") - .primaryKey() - .default(sql`xata_id()`), + id: text("xata_id").primaryKey().default(sql`xata_id()`), xataVersion: integer("xata_version"), accessToken: text("access_token").notNull(), refreshToken: text("refresh_token").notNull(), diff --git a/apps/api/src/scripts/genres.ts b/apps/api/src/scripts/genres.ts index fc5aa979..80e6c0ae 100644 --- a/apps/api/src/scripts/genres.ts +++ b/apps/api/src/scripts/genres.ts @@ -11,11 +11,11 @@ async function getSpotifyToken(): Promise { .from(tables.spotifyTokens) .leftJoin( tables.spotifyAccounts, - eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId) + eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId), ) .leftJoin( tables.spotifyApps, - eq(tables.spotifyApps.spotifyAppId, tables.spotifyTokens.spotifyAppId) + eq(tables.spotifyApps.spotifyAppId, tables.spotifyTokens.spotifyAppId), ) .where(eq(tables.spotifyAccounts.isBetaUser, true)) .execute(); @@ -24,7 +24,7 @@ async function getSpotifyToken(): Promise { spotifyTokens[Math.floor(Math.random() * spotifyTokens.length)]; const refreshToken = decrypt( record.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ); const accessToken = await fetch("https://accounts.spotify.com/api/token", { @@ -38,7 +38,7 @@ async function getSpotifyToken(): Promise { client_id: record.spotify_apps.spotifyAppId, client_secret: decrypt( record.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), }), }) @@ -60,7 +60,7 @@ async function getGenresAndPicture(artists) { headers: { Authorization: `Bearer ${token}`, }, - } + }, ) .then( (res) => @@ -73,7 +73,7 @@ async function getGenresAndPicture(artists) { images: Array<{ url: string }>; }>; }; - }> + }>, ) .then(async (data) => _.get(data, "artists.items.0")); diff --git a/apps/api/src/scripts/spotify.ts b/apps/api/src/scripts/spotify.ts index 5fdb5216..d52edc62 100644 --- a/apps/api/src/scripts/spotify.ts +++ b/apps/api/src/scripts/spotify.ts @@ -10,10 +10,10 @@ const clientSecret = args[1]; if (!clientId || !clientSecret) { console.error( - "Please provide Spotify Client ID and Client Secret as command line arguments" + "Please provide Spotify Client ID and Client Secret as command line arguments", ); console.log( - chalk.greenBright("Usage: ts-node spotify.ts ") + chalk.greenBright("Usage: ts-node spotify.ts "), ); process.exit(1); } diff --git a/apps/api/src/scripts/sync.ts b/apps/api/src/scripts/sync.ts index 8c63210d..d08b8cb8 100644 --- a/apps/api/src/scripts/sync.ts +++ b/apps/api/src/scripts/sync.ts @@ -143,7 +143,7 @@ if (args.includes("--background")) { } catch (err) { console.error( `Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`, - err + err, ); } } diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index cfeb13c0..7bdc6263 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -31,6 +31,6 @@ app.use(proxyMiddleware); app.listen(process.env.ROCKSKY_XPRC_PORT || 3004, () => { console.log( - `Rocksky XRPC API is running on port ${process.env.ROCKSKY_XRPC_PORT || 3004}` + `Rocksky XRPC API is running on port ${process.env.ROCKSKY_XRPC_PORT || 3004}`, ); }); diff --git a/apps/api/src/spotify/app.ts b/apps/api/src/spotify/app.ts index 019c9677..3be6abc0 100644 --- a/apps/api/src/spotify/app.ts +++ b/apps/api/src/spotify/app.ts @@ -24,7 +24,7 @@ app.use( limit: 10, // max Spotify API calls window: 15, // per 10 seconds keyPrefix: "spotify-ratelimit", - }) + }), ); app.get("/login", async (c) => { @@ -58,13 +58,13 @@ app.get("/login", async (c) => { .leftJoin(users, eq(spotifyAccounts.userId, users.id)) .leftJoin( spotifyApps, - eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId), ) .where( and( eq(spotifyAccounts.userId, user.id), - eq(spotifyAccounts.isBetaUser, true) - ) + eq(spotifyAccounts.isBetaUser, true), + ), ) .limit(1) .then((rows) => rows[0]); @@ -74,7 +74,7 @@ app.get("/login", async (c) => { 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}`; c.header( "Set-Cookie", - `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure` + `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`, ); return c.json({ redirectUrl }); }); @@ -110,13 +110,13 @@ app.get("/callback", async (c) => { .from(spotifyAccounts) .leftJoin( spotifyApps, - eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyAccounts.spotifyAppId, spotifyApps.spotifyAppId), ) .where( and( eq(spotifyAccounts.userId, user.id), - eq(spotifyAccounts.isBetaUser, true) - ) + eq(spotifyAccounts.isBetaUser, true), + ), ) .limit(1) .then((rows) => rows[0]); @@ -179,8 +179,8 @@ app.get("/callback", async (c) => { .where( and( eq(spotifyAccounts.userId, user.id), - eq(spotifyAccounts.isBetaUser, true) - ) + eq(spotifyAccounts.isBetaUser, true), + ), ) .limit(1) .then((rows) => rows[0]); @@ -230,7 +230,7 @@ app.post("/join", async (c) => { appId: spotifyApps.id, spotifyAppId: spotifyApps.spotifyAppId, accountCount: sql`COUNT(${spotifyAccounts.id})`.as( - "account_count" + "account_count", ), }) .from(spotifyApps) @@ -311,7 +311,7 @@ app.get("/currently-playing", async (c) => { } const cached = await ctx.redis.get( - `${spotifyAccount.spotifyAccount.email}:current` + `${spotifyAccount.spotifyAccount.email}:current`, ); if (!cached) { return c.json({}); @@ -321,7 +321,7 @@ app.get("/currently-playing", async (c) => { const sha256 = createHash("sha256") .update( - `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase() + `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase(), ) .digest("hex"); @@ -385,7 +385,7 @@ app.put("/pause", async (c) => { .from(spotifyTokens) .leftJoin( spotifyApps, - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) @@ -398,7 +398,7 @@ app.put("/pause", async (c) => { const refreshToken = decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ); // get new access token @@ -413,7 +413,7 @@ app.put("/pause", async (c) => { client_id: spotifyToken.spotify_apps.spotifyAppId, client_secret: decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), }), }); @@ -468,7 +468,7 @@ app.put("/play", async (c) => { .from(spotifyTokens) .leftJoin( spotifyApps, - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) @@ -481,7 +481,7 @@ app.put("/play", async (c) => { const refreshToken = decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ); // get new access token @@ -496,7 +496,7 @@ app.put("/play", async (c) => { client_id: spotifyToken.spotify_apps.spotifyAppId, client_secret: decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), }), }); @@ -551,7 +551,7 @@ app.post("/next", async (c) => { .from(spotifyTokens) .leftJoin( spotifyApps, - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) @@ -564,7 +564,7 @@ app.post("/next", async (c) => { const refreshToken = decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ); // get new access token @@ -579,7 +579,7 @@ app.post("/next", async (c) => { client_id: spotifyToken.spotify_apps.spotifyAppId, client_secret: decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), }), }); @@ -634,7 +634,7 @@ app.post("/previous", async (c) => { .from(spotifyTokens) .leftJoin( spotifyApps, - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) @@ -647,7 +647,7 @@ app.post("/previous", async (c) => { const refreshToken = decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ); // get new access token @@ -662,7 +662,7 @@ app.post("/previous", async (c) => { client_id: spotifyToken.spotify_apps.spotifyAppId, client_secret: decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), }), }); @@ -678,7 +678,7 @@ app.post("/previous", async (c) => { headers: { Authorization: `Bearer ${access_token}`, }, - } + }, ); if (response.status === 403) { @@ -720,7 +720,7 @@ app.put("/seek", async (c) => { .from(spotifyTokens) .leftJoin( spotifyApps, - eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId) + eq(spotifyTokens.spotifyAppId, spotifyApps.spotifyAppId), ) .where(eq(spotifyTokens.userId, user.id)) .limit(1) @@ -733,7 +733,7 @@ app.put("/seek", async (c) => { const refreshToken = decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ); // get new access token @@ -748,7 +748,7 @@ app.put("/seek", async (c) => { client_id: spotifyToken.spotify_apps.spotifyAppId, client_secret: decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), }), }); @@ -765,7 +765,7 @@ app.put("/seek", async (c) => { headers: { Authorization: `Bearer ${access_token}`, }, - } + }, ); if (response.status === 403) { diff --git a/apps/api/src/tealfm/index.ts b/apps/api/src/tealfm/index.ts index 491acbc8..8fef0f54 100644 --- a/apps/api/src/tealfm/index.ts +++ b/apps/api/src/tealfm/index.ts @@ -35,18 +35,16 @@ async function publishPlayingNow( record?.trackName === track.name)) && // diff in seconds less than 60 Math.abs( - new Date(record.playedTime).getTime() - - new Date(track.timestamp).getTime(), - ) < 60000 + new Date(record.playedTime).getTime() - + new Date(track.timestamp).getTime(), + ) < 60000 ); }); if (alreadyPlayed) { console.log( - `Track ${chalk.cyan(track.name)} by ${ - chalk.cyan( - track.artist.map((a) => a.name).join(", "), - ) - } already played recently. Skipping...`, + `Track ${chalk.cyan(track.name)} by ${chalk.cyan( + track.artist.map((a) => a.name).join(", "), + )} already played recently. Skipping...`, ); return; } diff --git a/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts b/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts index 743024fc..3e15059f 100644 --- a/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts +++ b/apps/api/src/xrpc/app/rocksky/actor/getProfile.ts @@ -33,7 +33,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.actor.getProfile({ auth: ctx.authVerifier, @@ -194,7 +194,7 @@ const retrieveProfile = ({ .from(tables.spotifyAccounts) .leftJoin( tables.users, - eq(tables.spotifyAccounts.userId, tables.users.id) + eq(tables.spotifyAccounts.userId, tables.users.id), ) .where(eq(tables.users.did, did)) .execute() @@ -204,7 +204,7 @@ const retrieveProfile = ({ .from(tables.spotifyTokens) .leftJoin( tables.users, - eq(tables.spotifyTokens.userId, tables.users.id) + eq(tables.spotifyTokens.userId, tables.users.id), ) .where(eq(tables.users.did, did)) .execute() @@ -214,7 +214,7 @@ const retrieveProfile = ({ .from(tables.googleDriveAccounts) .leftJoin( tables.users, - eq(tables.googleDriveAccounts.userId, tables.users.id) + eq(tables.googleDriveAccounts.userId, tables.users.id), ) .where(eq(tables.users.did, did)) .execute() @@ -224,7 +224,7 @@ const retrieveProfile = ({ .from(tables.dropboxAccounts) .leftJoin( tables.users, - eq(tables.dropboxAccounts.userId, tables.users.id) + eq(tables.dropboxAccounts.userId, tables.users.id), ) .where(eq(tables.users.did, did)) .execute() @@ -280,8 +280,8 @@ const refreshProfile = ([ xata_createdat: profile.user.createdAt.toISOString(), xata_updatedat: profile.user.updatedAt.toISOString(), xata_version: 1, - }) - ) + }), + ), ); } else { // Update existing user in background if handle or avatar or displayName changed @@ -314,8 +314,8 @@ const refreshProfile = ([ xata_createdat: profile.user.createdAt.toISOString(), xata_updatedat: new Date().toISOString(), xata_version: (profile.user.xataVersion || 1) + 1, - }) - ) + }), + ), ); } } diff --git a/apps/api/src/xrpc/app/rocksky/artist/getArtists.ts b/apps/api/src/xrpc/app/rocksky/artist/getArtists.ts index d220c8fc..f418eb41 100644 --- a/apps/api/src/xrpc/app/rocksky/artist/getArtists.ts +++ b/apps/api/src/xrpc/app/rocksky/artist/getArtists.ts @@ -46,6 +46,7 @@ const retrieve = ({ skip: params.offset || 0, take: params.limit || 100, }, + names: params.names, }); return { data: response.data, ctx }; }, diff --git a/apps/api/src/xrpc/app/rocksky/scrobble/getScrobble.ts b/apps/api/src/xrpc/app/rocksky/scrobble/getScrobble.ts index 926f9e8c..41b9370f 100644 --- a/apps/api/src/xrpc/app/rocksky/scrobble/getScrobble.ts +++ b/apps/api/src/xrpc/app/rocksky/scrobble/getScrobble.ts @@ -6,7 +6,7 @@ import type { ScrobbleViewDetailed } from "lexicon/types/app/rocksky/scrobble/de import type { QueryParams } from "lexicon/types/app/rocksky/scrobble/getScrobble"; import * as R from "ramda"; import tables from "schema"; -import { SelectAlbum } from "schema/albums"; +import type { SelectAlbum } from "schema/albums"; import type { SelectScrobble } from "schema/scrobbles"; import type { SelectTrack } from "schema/tracks"; import type { SelectUser } from "schema/users"; @@ -22,7 +22,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error("Error retrieving scrobble:", err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.scrobble.getScrobble({ handler: async ({ params }) => { @@ -63,7 +63,7 @@ const retrieve = ({ .from(tables.scrobbles) .leftJoin( tables.tracks, - eq(tables.tracks.id, tables.scrobbles.trackId) + eq(tables.tracks.id, tables.scrobbles.trackId), ) .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)) .where(eq(tables.scrobbles.trackId, scrobble?.tracks.id)) @@ -75,7 +75,7 @@ const retrieve = ({ .from(tables.scrobbles) .leftJoin( tables.tracks, - eq(tables.scrobbles.trackId, tables.tracks.id) + eq(tables.scrobbles.trackId, tables.tracks.id), ) .where(eq(tables.scrobbles.trackId, scrobble?.tracks.id)) .execute() diff --git a/apps/api/src/xrpc/app/rocksky/spotify/next.ts b/apps/api/src/xrpc/app/rocksky/spotify/next.ts index 6ab4158d..6265f319 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/next.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/next.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.spotify.next({ auth: ctx.authVerifier, @@ -71,18 +71,21 @@ const withSpotifyRefreshToken = ({ .from(tables.spotifyTokens) .leftJoin( tables.spotifyApps, - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + eq( + tables.spotifyTokens.spotifyAppId, + tables.spotifyApps.spotifyAppId, + ), ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() .then(([spotifyToken]) => [ decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), spotifyToken.spotify_apps.spotifyAppId, ]) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/pause.ts b/apps/api/src/xrpc/app/rocksky/spotify/pause.ts index c63f3ced..08651994 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/pause.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/pause.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.spotify.pause({ auth: ctx.authVerifier, @@ -71,18 +71,21 @@ const withSpotifyRefreshToken = ({ .from(tables.spotifyTokens) .leftJoin( tables.spotifyApps, - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + eq( + tables.spotifyTokens.spotifyAppId, + tables.spotifyApps.spotifyAppId, + ), ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() .then(([spotifyToken]) => [ decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), spotifyToken.spotify_apps.spotifyAppId, ]) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/play.ts b/apps/api/src/xrpc/app/rocksky/spotify/play.ts index 23c3f15f..1808ad73 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/play.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/play.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.spotify.play({ auth: ctx.authVerifier, @@ -71,18 +71,21 @@ const withSpotifyRefreshToken = ({ .from(tables.spotifyTokens) .leftJoin( tables.spotifyApps, - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + eq( + tables.spotifyTokens.spotifyAppId, + tables.spotifyApps.spotifyAppId, + ), ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() .then(([spotifyToken]) => [ decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), spotifyToken.spotify_apps.spotifyAppId, ]) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/previous.ts b/apps/api/src/xrpc/app/rocksky/spotify/previous.ts index 2acc9499..ad632908 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/previous.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/previous.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.spotify.previous({ auth: ctx.authVerifier, @@ -71,18 +71,21 @@ const withSpotifyRefreshToken = ({ .from(tables.spotifyTokens) .leftJoin( tables.spotifyApps, - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + eq( + tables.spotifyTokens.spotifyAppId, + tables.spotifyApps.spotifyAppId, + ), ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() .then(([spotifyToken]) => [ decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), spotifyToken.spotify_apps.spotifyAppId, ]) diff --git a/apps/api/src/xrpc/app/rocksky/spotify/seek.ts b/apps/api/src/xrpc/app/rocksky/spotify/seek.ts index 6e8c4311..141fcbe7 100644 --- a/apps/api/src/xrpc/app/rocksky/spotify/seek.ts +++ b/apps/api/src/xrpc/app/rocksky/spotify/seek.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.spotify.seek({ auth: ctx.authVerifier, @@ -74,18 +74,21 @@ const withSpotifyRefreshToken = ({ .from(tables.spotifyTokens) .leftJoin( tables.spotifyApps, - eq(tables.spotifyTokens.spotifyAppId, tables.spotifyApps.spotifyAppId) + eq( + tables.spotifyTokens.spotifyAppId, + tables.spotifyApps.spotifyAppId, + ), ) .where(eq(tables.spotifyTokens.userId, user.id)) .execute() .then(([spotifyToken]) => [ decrypt( spotifyToken.spotify_tokens.refreshToken, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), decrypt( spotifyToken.spotify_apps.spotifySecret, - env.SPOTIFY_ENCRYPTION_KEY + env.SPOTIFY_ENCRYPTION_KEY, ), spotifyToken.spotify_apps.spotifyAppId, ]) @@ -150,7 +153,7 @@ const handleSeek = ({ headers: { Authorization: `Bearer ${accessToken}`, }, - } + }, ).then((res) => res.status), catch: (error) => new Error(`Failed to handle next action: ${error}`), }); diff --git a/crates/analytics/src/handlers/artists.rs b/crates/analytics/src/handlers/artists.rs index 452e00cd..fbbfad27 100644 --- a/crates/analytics/src/handlers/artists.rs +++ b/crates/analytics/src/handlers/artists.rs @@ -10,7 +10,7 @@ use crate::types::{ }; use actix_web::{web, HttpRequest, HttpResponse}; use anyhow::Error; -use duckdb::Connection; +use duckdb::{params_from_iter, Connection}; use tokio_stream::StreamExt; use crate::read_payload; @@ -26,87 +26,134 @@ pub async fn get_artists( let offset = pagination.skip.unwrap_or(0); let limit = pagination.take.unwrap_or(20); let did = params.user_did; + let names = params.names; let conn = conn.lock().unwrap(); - let mut stmt = match did { - Some(_) => conn.prepare( - r#" - SELECT a.*, - COUNT(*) AS play_count, - COUNT(DISTINCT s.user_id) AS unique_listeners - FROM user_artists ua - LEFT JOIN artists a ON ua.artist_id = a.id - LEFT JOIN users u ON ua.user_id = u.id - LEFT JOIN scrobbles s ON s.artist_id = a.id - WHERE u.did = ? OR u.handle = ? - GROUP BY a.* - ORDER BY play_count DESC OFFSET ? LIMIT ?; - "#, - )?, - None => conn.prepare( - "SELECT a.*, - COUNT(*) AS play_count, - COUNT(DISTINCT s.user_id) AS unique_listeners - FROM artists a - LEFT JOIN scrobbles s ON s.artist_id = a.id - GROUP BY a.* - ORDER BY play_count DESC OFFSET ? LIMIT ?", - )?, - }; - match did { - Some(did) => { - let artists = stmt.query_map( - [&did, &did, &limit.to_string(), &offset.to_string()], - |row| { - Ok(Artist { - id: row.get(0)?, - name: row.get(1)?, - biography: row.get(2)?, - born: row.get(3)?, - born_in: row.get(4)?, - died: row.get(5)?, - picture: row.get(6)?, - sha256: row.get(7)?, - spotify_link: row.get(8)?, - tidal_link: row.get(9)?, - youtube_link: row.get(10)?, - apple_music_link: row.get(11)?, - uri: row.get(12)?, - play_count: row.get(13)?, - unique_listeners: row.get(14)?, - }) - }, - )?; + // Build dynamic query and params based on filters + let (query, params_vec): (String, Vec>) = + match (did.as_ref(), names.as_ref()) { + // Both did and names provided + (Some(d), Some(n)) if !n.is_empty() => { + let placeholders = vec!["?"; n.len()].join(", "); + let query = format!( + r#" + SELECT a.*, + COUNT(*) AS play_count, + COUNT(DISTINCT s.user_id) AS unique_listeners + FROM user_artists ua + LEFT JOIN artists a ON ua.artist_id = a.id + LEFT JOIN users u ON ua.user_id = u.id + LEFT JOIN scrobbles s ON s.artist_id = a.id + WHERE (u.did = ? OR u.handle = ?) + AND a.name IN ({}) + GROUP BY a.* + ORDER BY play_count DESC + LIMIT ? OFFSET ? + "#, + placeholders + ); + let mut params: Vec> = + vec![Box::new(d.clone()), Box::new(d.clone())]; + for name in n { + params.push(Box::new(name.clone())); + } + params.push(Box::new(limit)); + params.push(Box::new(offset)); + (query, params) + } + // Only did provided + (Some(d), _) => { + let query = r#" + SELECT a.*, + COUNT(*) AS play_count, + COUNT(DISTINCT s.user_id) AS unique_listeners + FROM user_artists ua + LEFT JOIN artists a ON ua.artist_id = a.id + LEFT JOIN users u ON ua.user_id = u.id + LEFT JOIN scrobbles s ON s.artist_id = a.id + WHERE u.did = ? OR u.handle = ? + GROUP BY a.* + ORDER BY play_count DESC + LIMIT ? OFFSET ? + "# + .to_string(); + ( + query, + vec![ + Box::new(d.clone()), + Box::new(d.clone()), + Box::new(limit), + Box::new(offset), + ], + ) + } + // Only names provided + (None, Some(n)) if !n.is_empty() => { + let placeholders = vec!["?"; n.len()].join(", "); + let query = format!( + r#" + SELECT a.*, + COUNT(*) AS play_count, + COUNT(DISTINCT s.user_id) AS unique_listeners + FROM artists a + LEFT JOIN scrobbles s ON s.artist_id = a.id + WHERE a.name IN ({}) + GROUP BY a.* + ORDER BY play_count DESC + LIMIT ? OFFSET ? + "#, + placeholders + ); + let mut params: Vec> = vec![]; + for name in n { + params.push(Box::new(name.clone())); + } + params.push(Box::new(limit)); + params.push(Box::new(offset)); + (query, params) + } + // No filters + (None, _) => { + let query = r#" + SELECT a.*, + COUNT(*) AS play_count, + COUNT(DISTINCT s.user_id) AS unique_listeners + FROM artists a + LEFT JOIN scrobbles s ON s.artist_id = a.id + GROUP BY a.* + ORDER BY play_count DESC + LIMIT ? OFFSET ? + "# + .to_string(); + (query, vec![Box::new(limit), Box::new(offset)]) + } + }; - let artists: Result, _> = artists.collect(); - Ok(HttpResponse::Ok().json(artists?)) - } - None => { - let artists = stmt.query_map([limit, offset], |row| { - Ok(Artist { - id: row.get(0)?, - name: row.get(1)?, - biography: row.get(2)?, - born: row.get(3)?, - born_in: row.get(4)?, - died: row.get(5)?, - picture: row.get(6)?, - sha256: row.get(7)?, - spotify_link: row.get(8)?, - tidal_link: row.get(9)?, - youtube_link: row.get(10)?, - apple_music_link: row.get(11)?, - uri: row.get(12)?, - play_count: row.get(13)?, - unique_listeners: row.get(14)?, - }) - })?; + // Prepare and execute query + let mut stmt = conn.prepare(&query)?; + let artists = stmt.query_map(params_from_iter(params_vec.iter()), |row| { + Ok(Artist { + id: row.get(0)?, + name: row.get(1)?, + biography: row.get(2)?, + born: row.get(3)?, + born_in: row.get(4)?, + died: row.get(5)?, + picture: row.get(6)?, + sha256: row.get(7)?, + spotify_link: row.get(8)?, + tidal_link: row.get(9)?, + youtube_link: row.get(10)?, + apple_music_link: row.get(11)?, + uri: row.get(12)?, + play_count: row.get(14)?, + unique_listeners: row.get(15)?, + }) + })?; - let artists: Result, _> = artists.collect(); - Ok(HttpResponse::Ok().json(artists?)) - } - } + let artists: Result, _> = artists.collect(); + Ok(HttpResponse::Ok().json(artists?)) } pub async fn get_top_artists( diff --git a/crates/analytics/src/types/artist.rs b/crates/analytics/src/types/artist.rs index 1e3e8c24..80af8bd7 100644 --- a/crates/analytics/src/types/artist.rs +++ b/crates/analytics/src/types/artist.rs @@ -60,6 +60,7 @@ pub struct ArtistListener { pub struct GetArtistsParams { pub user_did: Option, pub pagination: Option, + pub names: Option>, } #[derive(Debug, Serialize, Deserialize, Default)] -- 2.51.2