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

Add artist tags and include in getActorArtists

Add optional tags array to artist lexicon, pkl and TypeScript defs.
Populate tags by querying artists.genres from the DB and merging
them into the analytics response in getActorArtists.

+71 -14
+12
apps/api/lexicons/artist/defs.json
··· 35 "type": "integer", 36 "description": "The number of unique listeners who have played the artist.", 37 "minimum": 0 38 } 39 } 40 }, ··· 71 "type": "integer", 72 "description": "The number of unique listeners who have played the artist.", 73 "minimum": 0 74 } 75 } 76 },
··· 35 "type": "integer", 36 "description": "The number of unique listeners who have played the artist.", 37 "minimum": 0 38 + }, 39 + "tags": { 40 + "type": "array", 41 + "items": { 42 + "type": "string" 43 + } 44 } 45 } 46 }, ··· 77 "type": "integer", 78 "description": "The number of unique listeners who have played the artist.", 79 "minimum": 0 80 + }, 81 + "tags": { 82 + "type": "array", 83 + "items": { 84 + "type": "string" 85 + } 86 } 87 } 88 },
+13 -3
apps/api/pkl/defs/artist/defs.pkl
··· 1 - amends "../../schema/lexicon.pkl" 2 3 lexicon = 1 4 id = "app.rocksky.artist.defs" ··· 44 minimum = 0 45 } 46 47 } 48 } 49 ··· 88 minimum = 0 89 } 90 91 } 92 } 93 ··· 110 description = "The number of times the song has been played." 111 minimum = 0 112 } 113 - 114 } 115 } 116 ··· 158 description = "The rank of the listener among all listeners of the artist." 159 minimum = 1 160 } 161 - 162 } 163 } 164
··· 1 + amends "../../schema/lexicon.pkl" 2 3 lexicon = 1 4 id = "app.rocksky.artist.defs" ··· 44 minimum = 0 45 } 46 47 + ["tags"] = new Array { 48 + type = "array" 49 + items = new StringType { 50 + type = "string" 51 + } 52 + } 53 } 54 } 55 ··· 94 minimum = 0 95 } 96 97 + ["tags"] = new Array { 98 + type = "array" 99 + items = new StringType { 100 + type = "string" 101 + } 102 + } 103 } 104 } 105 ··· 122 description = "The number of times the song has been played." 123 minimum = 0 124 } 125 } 126 } 127 ··· 169 description = "The rank of the listener among all listeners of the artist." 170 minimum = 1 171 } 172 } 173 } 174
+12
apps/api/src/lexicon/lexicons.ts
··· 1835 "The number of unique listeners who have played the artist.", 1836 minimum: 0, 1837 }, 1838 }, 1839 }, 1840 artistViewDetailed: { ··· 1871 description: 1872 "The number of unique listeners who have played the artist.", 1873 minimum: 0, 1874 }, 1875 }, 1876 },
··· 1835 "The number of unique listeners who have played the artist.", 1836 minimum: 0, 1837 }, 1838 + tags: { 1839 + type: "array", 1840 + items: { 1841 + type: "string", 1842 + }, 1843 + }, 1844 }, 1845 }, 1846 artistViewDetailed: { ··· 1877 description: 1878 "The number of unique listeners who have played the artist.", 1879 minimum: 0, 1880 + }, 1881 + tags: { 1882 + type: "array", 1883 + items: { 1884 + type: "string", 1885 + }, 1886 }, 1887 }, 1888 },
+2
apps/api/src/lexicon/types/app/rocksky/artist/defs.ts
··· 21 playCount?: number; 22 /** The number of unique listeners who have played the artist. */ 23 uniqueListeners?: number; 24 [k: string]: unknown; 25 } 26 ··· 51 playCount?: number; 52 /** The number of unique listeners who have played the artist. */ 53 uniqueListeners?: number; 54 [k: string]: unknown; 55 } 56
··· 21 playCount?: number; 22 /** The number of unique listeners who have played the artist. */ 23 uniqueListeners?: number; 24 + tags?: string[]; 25 [k: string]: unknown; 26 } 27 ··· 52 playCount?: number; 53 /** The number of unique listeners who have played the artist. */ 54 uniqueListeners?: number; 55 + tags?: string[]; 56 [k: string]: unknown; 57 } 58
+30 -9
apps/api/src/xrpc/app/rocksky/actor/getActorArtists.ts
··· 5 import type { QueryParams } from "lexicon/types/app/rocksky/actor/getActorArtists"; 6 import type { ArtistViewBasic } from "lexicon/types/app/rocksky/artist/defs"; 7 import { deepCamelCaseKeys } from "lib"; 8 9 export default function (server: Server, ctx: Context) { 10 const getActorArtists = (params: QueryParams) => ··· 38 ctx: Context; 39 }): Effect.Effect<{ data: Artist[] }, Error> => { 40 return Effect.tryPromise({ 41 - try: () => 42 - ctx.analytics.post("library.getTopArtists", { 43 - user_did: params.did, 44 - pagination: { 45 - skip: params.offset || 0, 46 - take: params.limit || 10, 47 }, 48 - start_date: params.startDate, 49 - end_date: params.endDate, 50 - }), 51 catch: (error) => new Error(`Failed to retrieve artists: ${error}`), 52 }); 53 }; ··· 68 sha256: string; 69 unique_listeners: number; 70 uri: string; 71 };
··· 5 import type { QueryParams } from "lexicon/types/app/rocksky/actor/getActorArtists"; 6 import type { ArtistViewBasic } from "lexicon/types/app/rocksky/artist/defs"; 7 import { deepCamelCaseKeys } from "lib"; 8 + import { inArray } from "drizzle-orm"; 9 + import tables from "schema"; 10 + import { indexBy, prop } from "ramda"; 11 12 export default function (server: Server, ctx: Context) { 13 const getActorArtists = (params: QueryParams) => ··· 41 ctx: Context; 42 }): Effect.Effect<{ data: Artist[] }, Error> => { 43 return Effect.tryPromise({ 44 + try: async () => { 45 + const response = await ctx.analytics.post<Artist[]>( 46 + "library.getTopArtists", 47 + { 48 + user_did: params.did, 49 + pagination: { 50 + skip: params.offset || 0, 51 + take: params.limit || 10, 52 + }, 53 + start_date: params.startDate, 54 + end_date: params.endDate, 55 }, 56 + ); 57 + const ids = response.data.map((x) => x.id); 58 + const artists = await ctx.db 59 + .select() 60 + .from(tables.artists) 61 + .where(inArray(tables.artists.id, ids)) 62 + .execute(); 63 + const indexedArtists = indexBy(prop("id"), artists); 64 + return { 65 + data: response.data.map((x) => ({ 66 + ...x, 67 + tags: indexedArtists[x.id]?.genres, 68 + })), 69 + }; 70 + }, 71 catch: (error) => new Error(`Failed to retrieve artists: ${error}`), 72 }); 73 }; ··· 88 sha256: string; 89 unique_listeners: number; 90 uri: string; 91 + tags?: string[]; 92 };
+1 -1
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
··· 13 import axios from "axios"; 14 import type { HandlerAuth } from "@atproto/xrpc-server"; 15 import { env } from "lib/env"; 16 - import { SelectArtist } from "schema/artists"; 17 18 export default function (server: Server, ctx: Context) { 19 const getFeed = (params: QueryParams, auth: HandlerAuth) =>
··· 13 import axios from "axios"; 14 import type { HandlerAuth } from "@atproto/xrpc-server"; 15 import { env } from "lib/env"; 16 + import type { SelectArtist } from "schema/artists"; 17 18 export default function (server: Server, ctx: Context) { 19 const getFeed = (params: QueryParams, auth: HandlerAuth) =>
+1 -1
apps/api/src/xrpc/app/rocksky/scrobble/getScrobbles.ts
··· 10 import type { SelectScrobble } from "schema/scrobbles"; 11 import type { SelectTrack } from "schema/tracks"; 12 import type { SelectUser } from "schema/users"; 13 - import { SelectArtist } from "schema/artists"; 14 15 export default function (server: Server, ctx: Context) { 16 const getScrobbles = (params: QueryParams) =>
··· 10 import type { SelectScrobble } from "schema/scrobbles"; 11 import type { SelectTrack } from "schema/tracks"; 12 import type { SelectUser } from "schema/users"; 13 + import type { SelectArtist } from "schema/artists"; 14 15 export default function (server: Server, ctx: Context) { 16 const getScrobbles = (params: QueryParams) =>