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

Update playlist item lexicon and song tags

Generate TypeScript types for app.rocksky.playlistItem and set its
required fields to createdAt, track, order, and subject. Add a
tags:string[] to song defs. In the feed handler, join artists and map
artists.genres into the response tags field.

+100 -5
+3 -2
apps/api/lexicons/playlist/playlistItem.json
··· 9 9 "record": { 10 10 "type": "object", 11 11 "required": [ 12 - "name", 13 12 "createdAt", 14 - "track" 13 + "track", 14 + "order", 15 + "subject" 15 16 ], 16 17 "properties": { 17 18 "subject": {
+6
apps/api/lexicons/song/defs.json
··· 71 71 "type": "string", 72 72 "description": "The SHA256 hash of the song." 73 73 }, 74 + "tags": { 75 + "type": "array", 76 + "items": { 77 + "type": "string" 78 + } 79 + }, 74 80 "createdAt": { 75 81 "type": "string", 76 82 "description": "The timestamp when the song was created.",
+1 -1
apps/api/pkl/defs/playlist/playlistItem.pkl
··· 10 10 key = "tid" 11 11 `record` { 12 12 type = "object" 13 - required = List("name", "createdAt", "track") 13 + required = List("createdAt", "track", "order", "subject") 14 14 properties { 15 15 ["subject"] = new Ref { 16 16 type = "ref"
+6
apps/api/pkl/defs/song/defs.pkl
··· 72 72 type = "string" 73 73 description = "The SHA256 hash of the song." 74 74 } 75 + ["tags"] = new Array { 76 + type = "array" 77 + items = new StringType { 78 + type = "string" 79 + } 80 + } 75 81 ["createdAt"] = new StringType { 76 82 type = "string" 77 83 format = "datetime"
+43
apps/api/src/lexicon/lexicons.ts
··· 4093 4093 }, 4094 4094 }, 4095 4095 }, 4096 + AppRockskyPlaylistItem: { 4097 + lexicon: 1, 4098 + id: "app.rocksky.playlistItem", 4099 + defs: { 4100 + main: { 4101 + type: "record", 4102 + description: 4103 + "A playlist item represents a single entry in a playlist, containing metadata and references to media content.", 4104 + key: "tid", 4105 + record: { 4106 + type: "object", 4107 + required: ["createdAt", "track", "order", "subject"], 4108 + properties: { 4109 + subject: { 4110 + type: "ref", 4111 + ref: "lex:com.atproto.repo.strongRef", 4112 + }, 4113 + createdAt: { 4114 + type: "string", 4115 + description: "The date the playlist was created.", 4116 + format: "datetime", 4117 + }, 4118 + track: { 4119 + type: "ref", 4120 + ref: "lex:app.rocksky.song.defs#songViewBasic", 4121 + }, 4122 + order: { 4123 + type: "integer", 4124 + description: "The order of the item in the playlist.", 4125 + minimum: 0, 4126 + }, 4127 + }, 4128 + }, 4129 + }, 4130 + }, 4131 + }, 4096 4132 AppRockskyPlaylistRemovePlaylist: { 4097 4133 lexicon: 1, 4098 4134 id: "app.rocksky.playlist.removePlaylist", ··· 5397 5433 type: "string", 5398 5434 description: "The SHA256 hash of the song.", 5399 5435 }, 5436 + tags: { 5437 + type: "array", 5438 + items: { 5439 + type: "string", 5440 + }, 5441 + }, 5400 5442 createdAt: { 5401 5443 type: "string", 5402 5444 description: "The timestamp when the song was created.", ··· 6048 6090 AppRockskyPlaylistInsertDirectory: "app.rocksky.playlist.insertDirectory", 6049 6091 AppRockskyPlaylistInsertFiles: "app.rocksky.playlist.insertFiles", 6050 6092 AppRockskyPlaylist: "app.rocksky.playlist", 6093 + AppRockskyPlaylistItem: "app.rocksky.playlistItem", 6051 6094 AppRockskyPlaylistRemovePlaylist: "app.rocksky.playlist.removePlaylist", 6052 6095 AppRockskyPlaylistRemoveTrack: "app.rocksky.playlist.removeTrack", 6053 6096 AppRockskyPlaylistStartPlaylist: "app.rocksky.playlist.startPlaylist",
+32
apps/api/src/lexicon/types/app/rocksky/playlistItem.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../lexicons"; 6 + import { isObj, hasProp } from "../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import type * as ComAtprotoRepoStrongRef from "../../com/atproto/repo/strongRef"; 9 + import type * as AppRockskySongDefs from "./song/defs"; 10 + 11 + export interface Record { 12 + subject: ComAtprotoRepoStrongRef.Main; 13 + /** The date the playlist was created. */ 14 + createdAt: string; 15 + track: AppRockskySongDefs.SongViewBasic; 16 + /** The order of the item in the playlist. */ 17 + order: number; 18 + [k: string]: unknown; 19 + } 20 + 21 + export function isRecord(v: unknown): v is Record { 22 + return ( 23 + isObj(v) && 24 + hasProp(v, "$type") && 25 + (v.$type === "app.rocksky.playlistItem#main" || 26 + v.$type === "app.rocksky.playlistItem") 27 + ); 28 + } 29 + 30 + export function validateRecord(v: unknown): ValidationResult { 31 + return lexicons.validate("app.rocksky.playlistItem#main", v); 32 + }
+1
apps/api/src/lexicon/types/app/rocksky/song/defs.ts
··· 37 37 artistUri?: string; 38 38 /** The SHA256 hash of the song. */ 39 39 sha256?: string; 40 + tags?: string[]; 40 41 /** The timestamp when the song was created. */ 41 42 createdAt?: string; 42 43 [k: string]: unknown;
+8 -2
apps/api/src/xrpc/app/rocksky/feed/getFeed.ts
··· 13 13 import axios from "axios"; 14 14 import type { HandlerAuth } from "@atproto/xrpc-server"; 15 15 import { env } from "lib/env"; 16 + import { SelectArtist } from "schema/artists"; 16 17 17 18 export default function (server: Server, ctx: Context) { 18 19 const getFeed = (params: QueryParams, auth: HandlerAuth) => ··· 101 102 .from(tables.scrobbles) 102 103 .leftJoin(tables.tracks, eq(tables.scrobbles.trackId, tables.tracks.id)) 103 104 .leftJoin(tables.users, eq(tables.scrobbles.userId, tables.users.id)) 105 + .leftJoin( 106 + tables.artists, 107 + eq(tables.tracks.artistUri, tables.artists.uri), 108 + ) 104 109 .where(inArray(tables.scrobbles.uri, uris)) 105 110 .orderBy(desc(tables.scrobbles.timestamp)) 106 111 .execute(); ··· 169 174 ): Effect.Effect<FeedView, never> => { 170 175 return Effect.sync(() => ({ 171 176 feed: data.scrobbles.map( 172 - ({ scrobbles, tracks, users, likesCount, liked }) => ({ 177 + ({ scrobbles, tracks, users, likesCount, liked, artists }) => ({ 173 178 scrobble: { 174 179 ...R.omit(["albumArt", "id", "lyrics"])(tracks), 175 180 cover: tracks.albumArt, ··· 178 183 userDisplayName: users.displayName, 179 184 userAvatar: users.avatar, 180 185 uri: scrobbles.uri, 181 - tags: [], 186 + tags: artists.genres, 182 187 likesCount, 183 188 liked, 184 189 trackUri: tracks.uri, ··· 196 201 scrobbles: SelectScrobble; 197 202 tracks: SelectTrack; 198 203 users: SelectUser; 204 + artists: SelectArtist; 199 205 likesCount: number; 200 206 liked: boolean; 201 207 }[];