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

Add tags from artist genres to albums and songs

Add tags array to album and song lexicons, PKL files, and generated
TypeScript types. Populate tags in getAlbum, getSong, and getScrobble by
joining artists and using artist.genres. Also replace Math.pow with the
exponent operator for the Spotify retry delay calculation.

+71 -13
+6
apps/api/lexicons/album/defs.json
··· 108 108 "description": "The number of unique listeners who have played the album.", 109 109 "minimum": 0 110 110 }, 111 + "tags": { 112 + "type": "array", 113 + "items": { 114 + "type": "string" 115 + } 116 + }, 111 117 "tracks": { 112 118 "type": "array", 113 119 "items": {
+6
apps/api/lexicons/song/defs.json
··· 147 147 "type": "string", 148 148 "description": "The SHA256 hash of the song." 149 149 }, 150 + "tags": { 151 + "type": "array", 152 + "items": { 153 + "type": "string" 154 + } 155 + }, 150 156 "createdAt": { 151 157 "type": "string", 152 158 "description": "The timestamp when the song was created.",
+9 -2
apps/api/pkl/defs/album/defs.pkl
··· 1 - amends "../../schema/lexicon.pkl" 1 + amends "../../schema/lexicon.pkl" 2 2 3 3 lexicon = 1 4 4 id = "app.rocksky.album.defs" ··· 130 130 minimum = 0 131 131 } 132 132 133 + ["tags"] = new Array { 134 + type = "array" 135 + items = new StringType { 136 + type = "string" 137 + } 138 + } 139 + 133 140 ["tracks"] = new Array { 134 141 type = "array" 135 142 items = new Ref { ··· 139 146 } 140 147 } 141 148 } 142 - } 149 + }
+8 -2
apps/api/pkl/defs/song/defs.pkl
··· 1 - amends "../../schema/lexicon.pkl" 1 + amends "../../schema/lexicon.pkl" 2 2 3 3 lexicon = 1 4 4 id = "app.rocksky.song.defs" ··· 148 148 type = "string" 149 149 description = "The SHA256 hash of the song." 150 150 } 151 + ["tags"] = new Array { 152 + type = "array" 153 + items = new StringType { 154 + type = "string" 155 + } 156 + } 151 157 ["createdAt"] = new StringType { 152 158 type = "string" 153 159 format = "datetime" ··· 155 161 } 156 162 } 157 163 } 158 - } 164 + }
+12
apps/api/src/lexicon/lexicons.ts
··· 1436 1436 "The number of unique listeners who have played the album.", 1437 1437 minimum: 0, 1438 1438 }, 1439 + tags: { 1440 + type: "array", 1441 + items: { 1442 + type: "string", 1443 + }, 1444 + }, 1439 1445 tracks: { 1440 1446 type: "array", 1441 1447 items: { ··· 5467 5473 sha256: { 5468 5474 type: "string", 5469 5475 description: "The SHA256 hash of the song.", 5476 + }, 5477 + tags: { 5478 + type: "array", 5479 + items: { 5480 + type: "string", 5481 + }, 5470 5482 }, 5471 5483 createdAt: { 5472 5484 type: "string",
+1
apps/api/src/lexicon/types/app/rocksky/album/defs.ts
··· 68 68 playCount?: number; 69 69 /** The number of unique listeners who have played the album. */ 70 70 uniqueListeners?: number; 71 + tags?: string[]; 71 72 tracks?: AppRockskySongDefsSongViewBasic.Main[]; 72 73 [k: string]: unknown; 73 74 }
+1
apps/api/src/lexicon/types/app/rocksky/song/defs.ts
··· 85 85 artistUri?: string; 86 86 /** The SHA256 hash of the song. */ 87 87 sha256?: string; 88 + tags?: string[]; 88 89 /** The timestamp when the song was created. */ 89 90 createdAt?: string; 90 91 [k: string]: unknown;
+8 -3
apps/api/src/xrpc/app/rocksky/album/getAlbum.ts
··· 10 10 import tables from "schema"; 11 11 import type { SelectAlbum } from "schema/albums"; 12 12 import type { SelectTrack } from "schema/tracks"; 13 + import type { SelectArtist } from "schema/artists"; 13 14 14 15 export default function (server: Server, ctx: Context) { 15 16 const getAlbum = (params) => ··· 38 39 const retrieve = ({ params, ctx }: { params: QueryParams; ctx: Context }) => { 39 40 return Effect.tryPromise({ 40 41 try: async () => { 41 - const album = await ctx.db 42 + const { albums: album, artists: artist } = await ctx.db 42 43 .select() 43 44 .from(tables.userAlbums) 44 45 .leftJoin( 45 46 tables.albums, 46 47 eq(tables.userAlbums.albumId, tables.albums.id), 47 48 ) 49 + .leftJoin(tables.artists, eq(tables.albums.artist, tables.artists.name)) 48 50 .where( 49 51 or( 50 52 eq(tables.userAlbums.uri, params.uri), ··· 52 54 ), 53 55 ) 54 56 .execute() 55 - .then((rows) => rows[0]?.albums); 57 + .then((rows) => rows[0]); 56 58 return Promise.all([ 57 59 Promise.resolve(album), 60 + Promise.resolve(artist), 58 61 ctx.db 59 62 .select() 60 63 .from(tables.albumTracks) ··· 98 101 }); 99 102 }; 100 103 101 - const presentation = ([album, tracks, uniqueListeners, playCount]: [ 104 + const presentation = ([album, artist, tracks, uniqueListeners, playCount]: [ 102 105 SelectAlbum, 106 + SelectArtist, 103 107 SelectTrack[], 104 108 number, 105 109 number, 106 110 ]): Effect.Effect<AlbumViewDetailed, never> => { 107 111 return Effect.sync(() => ({ 108 112 ...album, 113 + tags: artist.genres, 109 114 tracks, 110 115 playCount, 111 116 uniqueListeners,
+8 -2
apps/api/src/xrpc/app/rocksky/scrobble/getScrobble.ts
··· 11 11 import type { SelectScrobble } from "schema/scrobbles"; 12 12 import type { SelectTrack } from "schema/tracks"; 13 13 import type { SelectUser } from "schema/users"; 14 + import type { SelectArtist } from "schema/artists"; 14 15 15 16 export default function (server: Server, ctx: Context) { 16 17 const getScrobble = (params) => ··· 51 52 .leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id)) 52 53 .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)) 53 54 .leftJoin(tables.albums, eq(tables.scrobbles.albumId, tables.albums.id)) 55 + .leftJoin( 56 + tables.artists, 57 + eq(tables.scrobbles.artistId, tables.artists.id), 58 + ) 54 59 .where(eq(tables.scrobbles.uri, params.uri)) 55 60 .execute() 56 61 .then((rows) => rows[0]); ··· 88 93 }; 89 94 90 95 const presentation = ([ 91 - { scrobbles, tracks, users, albums }, 96 + { scrobbles, tracks, users, albums, artists }, 92 97 listeners, 93 98 scrobblesCount, 94 99 ]: [Scrobble | undefined, number, number]): Effect.Effect< ··· 102 107 date: scrobbles.timestamp.toISOString(), 103 108 user: users.handle, 104 109 uri: scrobbles.uri, 105 - tags: [], 110 + tags: artists.genres, 106 111 listeners, 107 112 scrobbles: scrobblesCount, 108 113 id: scrobbles.id, ··· 114 119 tracks: SelectTrack; 115 120 users: SelectUser; 116 121 albums: SelectAlbum; 122 + artists: SelectArtist; 117 123 };
+11 -3
apps/api/src/xrpc/app/rocksky/song/getSong.ts
··· 7 7 import type { QueryParams } from "lexicon/types/app/rocksky/song/getSong"; 8 8 import tables from "schema"; 9 9 import type { SelectTrack } from "schema/tracks"; 10 + import type { SelectArtist } from "schema/artists"; 10 11 11 12 export default function (server: Server, ctx: Context) { 12 13 const getSong = (params) => ··· 35 36 const retrieve = ({ params, ctx }: { params: QueryParams; ctx: Context }) => { 36 37 return Effect.tryPromise({ 37 38 try: async () => { 38 - const track = await ctx.db 39 + const { tracks: track, artists: artist } = await ctx.db 39 40 .select() 40 41 .from(tables.userTracks) 41 42 .leftJoin( 42 43 tables.tracks, 43 44 eq(tables.userTracks.trackId, tables.tracks.id), 45 + ) 46 + .leftJoin( 47 + tables.artists, 48 + eq(tables.tracks.albumArtist, tables.artists.name), 44 49 ) 45 50 .where( 46 51 or( ··· 49 54 ), 50 55 ) 51 56 .execute() 52 - .then(([row]) => row?.tracks); 57 + .then(([row]) => row); 53 58 return Promise.all([ 54 59 Promise.resolve(track), 60 + Promise.resolve(artist), 55 61 ctx.db 56 62 .select({ 57 63 count: count(), ··· 72 78 }); 73 79 }; 74 80 75 - const presentation = ([track, uniqueListeners, playCount]: [ 81 + const presentation = ([track, artist, uniqueListeners, playCount]: [ 76 82 SelectTrack, 83 + SelectArtist, 77 84 number, 78 85 number, 79 86 ]): Effect.Effect<SongViewDetailed, never> => { 80 87 return Effect.sync(() => ({ 81 88 ...track, 89 + tags: artist.genres, 82 90 playCount, 83 91 uniqueListeners, 84 92 createdAt: track.createdAt.toISOString(),
+1 -1
apps/api/src/xrpc/app/rocksky/song/matchSong.ts
··· 246 246 errorMessage.includes("ETIMEDOUT"); 247 247 248 248 if (isTimeout && attempt < MAX_SPOTIFY_RETRIES - 1) { 249 - const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt); 249 + const delay = INITIAL_RETRY_DELAY_MS * 2 ** attempt; 250 250 consola.warn( 251 251 `Spotify API timeout, retrying... attempt=${attempt + 1}, max_attempts=${MAX_SPOTIFY_RETRIES}, delay_ms=${delay}, operation=${operation}`, 252 252 );