+30
-1
apps/api/lexicons/artist/defs.json
+30
-1
apps/api/lexicons/artist/defs.json
···
74
74
}
75
75
}
76
76
},
77
+
"songViewBasic": {
78
+
"type": "object",
79
+
"properties": {
80
+
"uri": {
81
+
"type": "string",
82
+
"description": "The URI of the song.",
83
+
"format": "at-uri"
84
+
},
85
+
"title": {
86
+
"type": "string",
87
+
"description": "The title of the song."
88
+
},
89
+
"playCount": {
90
+
"type": "integer",
91
+
"description": "The number of times the song has been played.",
92
+
"minimum": 0
93
+
}
94
+
}
95
+
},
77
96
"listenerViewBasic": {
78
97
"type": "object",
79
98
"properties": {
···
100
119
},
101
120
"mostListenedSong": {
102
121
"type": "ref",
103
-
"ref": "app.rocksky.song.defs#songViewBasic"
122
+
"ref": "app.rocksky.artist.defs#songViewBasic"
123
+
},
124
+
"totalPlays": {
125
+
"type": "integer",
126
+
"description": "The total number of plays by the listener.",
127
+
"minimum": 0
128
+
},
129
+
"rank": {
130
+
"type": "integer",
131
+
"description": "The rank of the listener among all listeners of the artist.",
132
+
"minimum": 1
104
133
}
105
134
}
106
135
}
+36
-1
apps/api/pkl/defs/artist/defs.pkl
+36
-1
apps/api/pkl/defs/artist/defs.pkl
···
91
91
}
92
92
}
93
93
94
+
["songViewBasic"] {
95
+
type = "object"
96
+
properties {
97
+
["uri"] = new StringType {
98
+
type = "string"
99
+
format = "at-uri"
100
+
description = "The URI of the song."
101
+
}
102
+
103
+
["title"] = new StringType {
104
+
type = "string"
105
+
description = "The title of the song."
106
+
}
107
+
108
+
["playCount"] = new IntegerType {
109
+
type = "integer"
110
+
description = "The number of times the song has been played."
111
+
minimum = 0
112
+
}
113
+
114
+
}
115
+
}
116
+
94
117
["listenerViewBasic"] {
95
118
type = "object"
96
119
properties {
···
121
144
}
122
145
123
146
["mostListenedSong"] = new Ref {
124
-
ref = "app.rocksky.song.defs#songViewBasic"
147
+
ref = "app.rocksky.artist.defs#songViewBasic"
148
+
}
149
+
150
+
["totalPlays"] = new IntegerType {
151
+
type = "integer"
152
+
description = "The total number of plays by the listener."
153
+
minimum = 0
154
+
}
155
+
156
+
["rank"] = new IntegerType {
157
+
type = "integer"
158
+
description = "The rank of the listener among all listeners of the artist."
159
+
minimum = 1
125
160
}
126
161
127
162
}
+31
-1
apps/api/src/lexicon/lexicons.ts
+31
-1
apps/api/src/lexicon/lexicons.ts
···
1066
1066
},
1067
1067
},
1068
1068
},
1069
+
songViewBasic: {
1070
+
type: 'object',
1071
+
properties: {
1072
+
uri: {
1073
+
type: 'string',
1074
+
description: 'The URI of the song.',
1075
+
format: 'at-uri',
1076
+
},
1077
+
title: {
1078
+
type: 'string',
1079
+
description: 'The title of the song.',
1080
+
},
1081
+
playCount: {
1082
+
type: 'integer',
1083
+
description: 'The number of times the song has been played.',
1084
+
minimum: 0,
1085
+
},
1086
+
},
1087
+
},
1069
1088
listenerViewBasic: {
1070
1089
type: 'object',
1071
1090
properties: {
···
1092
1111
},
1093
1112
mostListenedSong: {
1094
1113
type: 'ref',
1095
-
ref: 'lex:app.rocksky.song.defs#songViewBasic',
1114
+
ref: 'lex:app.rocksky.artist.defs#songViewBasic',
1115
+
},
1116
+
totalPlays: {
1117
+
type: 'integer',
1118
+
description: 'The total number of plays by the listener.',
1119
+
minimum: 0,
1120
+
},
1121
+
rank: {
1122
+
type: 'integer',
1123
+
description:
1124
+
'The rank of the listener among all listeners of the artist.',
1125
+
minimum: 1,
1096
1126
},
1097
1127
},
1098
1128
},
+27
-2
apps/api/src/lexicon/types/app/rocksky/artist/defs.ts
+27
-2
apps/api/src/lexicon/types/app/rocksky/artist/defs.ts
···
5
5
import { lexicons } from '../../../../lexicons'
6
6
import { isObj, hasProp } from '../../../../util'
7
7
import { CID } from 'multiformats/cid'
8
-
import type * as AppRockskySongDefs from '../song/defs'
9
8
10
9
export interface ArtistViewBasic {
11
10
/** The unique identifier of the artist. */
···
67
66
return lexicons.validate('app.rocksky.artist.defs#artistViewDetailed', v)
68
67
}
69
68
69
+
export interface SongViewBasic {
70
+
/** The URI of the song. */
71
+
uri?: string
72
+
/** The title of the song. */
73
+
title?: string
74
+
/** The number of times the song has been played. */
75
+
playCount?: number
76
+
[k: string]: unknown
77
+
}
78
+
79
+
export function isSongViewBasic(v: unknown): v is SongViewBasic {
80
+
return (
81
+
isObj(v) &&
82
+
hasProp(v, '$type') &&
83
+
v.$type === 'app.rocksky.artist.defs#songViewBasic'
84
+
)
85
+
}
86
+
87
+
export function validateSongViewBasic(v: unknown): ValidationResult {
88
+
return lexicons.validate('app.rocksky.artist.defs#songViewBasic', v)
89
+
}
90
+
70
91
export interface ListenerViewBasic {
71
92
/** The unique identifier of the actor. */
72
93
id?: string
···
78
99
displayName?: string
79
100
/** The URL of the listener's avatar image. */
80
101
avatar?: string
81
-
mostListenedSong?: AppRockskySongDefs.SongViewBasic
102
+
mostListenedSong?: SongViewBasic
103
+
/** The total number of plays by the listener. */
104
+
totalPlays?: number
105
+
/** The rank of the listener among all listeners of the artist. */
106
+
rank?: number
82
107
[k: string]: unknown
83
108
}
84
109
+83
apps/api/src/xrpc/app/rocksky/artist/getArtistListeners.ts
+83
apps/api/src/xrpc/app/rocksky/artist/getArtistListeners.ts
···
1
+
import type { Context } from "context";
2
+
import { Effect, pipe } from "effect";
3
+
import type { Server } from "lexicon";
4
+
import type { ListenerViewBasic } from "lexicon/types/app/rocksky/artist/defs";
5
+
import type { QueryParams } from "lexicon/types/app/rocksky/artist/getArtistListeners";
6
+
7
+
export default function (server: Server, ctx: Context) {
8
+
const getArtistListeners = (params) =>
9
+
pipe(
10
+
{ params, ctx },
11
+
retrieve,
12
+
Effect.flatMap(presentation),
13
+
Effect.retry({ times: 3 }),
14
+
Effect.timeout("10 seconds"),
15
+
Effect.catchAll((err) => {
16
+
console.error(err);
17
+
return Effect.succeed({ listeners: [] });
18
+
})
19
+
);
20
+
server.app.rocksky.artist.getArtistListeners({
21
+
handler: async ({ params }) => {
22
+
const result = await Effect.runPromise(getArtistListeners(params));
23
+
return {
24
+
encoding: "application/json",
25
+
body: result,
26
+
};
27
+
},
28
+
});
29
+
}
30
+
31
+
const retrieve = ({
32
+
params,
33
+
ctx,
34
+
}: {
35
+
params: QueryParams;
36
+
ctx: Context;
37
+
}): Effect.Effect<{ data: ArtistListener[] }, Error> => {
38
+
return Effect.tryPromise({
39
+
try: () =>
40
+
ctx.analytics.post("library.getArtistListeners", {
41
+
artist_id: params.uri,
42
+
}),
43
+
catch: (error) =>
44
+
new Error(`Failed to retrieve artist's listeners: ${error}`),
45
+
});
46
+
};
47
+
48
+
const presentation = ({
49
+
data,
50
+
}: {
51
+
data: ArtistListener[];
52
+
}): Effect.Effect<{ listeners: ListenerViewBasic[] }, never> => {
53
+
return Effect.sync(() => ({
54
+
listeners: data.map((item) => ({
55
+
id: item.user_id,
56
+
did: item.did,
57
+
handle: item.handle,
58
+
displayName: item.display_name,
59
+
avatar: item.avatar,
60
+
mostListenedSong: {
61
+
title: item.most_played_track,
62
+
uri: item.most_played_track_uri,
63
+
playCount: item.track_play_count,
64
+
},
65
+
totalPlays: item.total_artist_plays,
66
+
rank: item.listener_rank,
67
+
})),
68
+
}));
69
+
};
70
+
71
+
type ArtistListener = {
72
+
artist: string;
73
+
avatar: string;
74
+
did: string;
75
+
display_name: string;
76
+
handle: string;
77
+
listener_rank: number;
78
+
most_played_track: string;
79
+
most_played_track_uri: string;
80
+
total_artist_plays: number;
81
+
track_play_count: number;
82
+
user_id: string;
83
+
};