+2
-1
.dockerignore
+2
-1
.dockerignore
+2
-1
.gitignore
+2
-1
.gitignore
+6
-1
README.md
+6
-1
README.md
···
47
47
48
48
- Node.js (v22 or later)
49
49
- Rust
50
+
- Go
50
51
- Turbo
51
52
- Docker
52
53
- Wasm Pack https://rustwasm.github.io/wasm-pack/installer/
···
92
93
```bash
93
94
bun run dev:jetstream
94
95
```
95
-
9. Start the development server:
96
+
9. Start musicbrainz:
97
+
```bash
98
+
bun run mb
99
+
```
100
+
10. Start the development server:
96
101
```bash
97
102
turbo dev --filter=@rocksky/api --filter=@rocksky/web
98
103
```
+1
-1
apps/api/package.json
+1
-1
apps/api/package.json
···
5
5
"type": "module",
6
6
"module": "dist/index.js",
7
7
"scripts": {
8
-
"lexgen": "lex gen-server ./src/lexicon ./lexicons/**/* ./lexicons/*",
8
+
"lexgen": "lex gen-server ./src/lexicon ./src/tealfm/lexicons/teal/**/* ./lexicons/**/* ./lexicons/* ./src/tealfm/lexicons/**/*",
9
9
"dev": "concurrently 'tsx --watch ./src/index.ts' 'tsx --watch ./src/server.ts'",
10
10
"prod": "tsx ./src/index.ts",
11
11
"build": "pkgroll",
+12
-9
apps/api/src/bsky/app.ts
+12
-9
apps/api/src/bsky/app.ts
···
81
81
? Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 1000
82
82
: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7,
83
83
},
84
-
env.JWT_SECRET
84
+
env.JWT_SECRET,
85
85
);
86
86
ctx.kv.set(did, token);
87
87
} catch (err) {
···
93
93
.select()
94
94
.from(spotifyAccounts)
95
95
.where(
96
-
and(eq(spotifyAccounts.userId, did), eq(spotifyAccounts.isBetaUser, true))
96
+
and(
97
+
eq(spotifyAccounts.userId, did),
98
+
eq(spotifyAccounts.isBetaUser, true),
99
+
),
97
100
)
98
101
.limit(1)
99
102
.execute();
···
179
182
180
183
ctx.nc.publish(
181
184
"rocksky.user",
182
-
Buffer.from(JSON.stringify(deepSnakeCaseKeys(user)))
185
+
Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))),
183
186
);
184
187
185
188
await ctx.kv.set("lastUser", lastUser[0].id);
···
192
195
.where(
193
196
and(
194
197
eq(spotifyAccounts.userId, did),
195
-
eq(spotifyAccounts.isBetaUser, true)
196
-
)
198
+
eq(spotifyAccounts.isBetaUser, true),
199
+
),
197
200
)
198
201
.limit(1)
199
202
.execute(),
···
209
212
.where(
210
213
and(
211
214
eq(googleDriveAccounts.userId, did),
212
-
eq(googleDriveAccounts.isBetaUser, true)
213
-
)
215
+
eq(googleDriveAccounts.isBetaUser, true),
216
+
),
214
217
)
215
218
.limit(1)
216
219
.execute(),
···
220
223
.where(
221
224
and(
222
225
eq(dropboxAccounts.userId, did),
223
-
eq(dropboxAccounts.isBetaUser, true)
224
-
)
226
+
eq(dropboxAccounts.isBetaUser, true),
227
+
),
225
228
)
226
229
.limit(1)
227
230
.execute(),
+1
apps/api/src/context.ts
+1
apps/api/src/context.ts
···
31
31
dropbox: axios.create({ baseURL: env.DROPBOX }),
32
32
googledrive: axios.create({ baseURL: env.GOOGLE_DRIVE }),
33
33
tracklist: axios.create({ baseURL: env.TRACKLIST }),
34
+
musicbrainz: axios.create({ baseURL: env.MUSICBRAINZ_URL }),
34
35
redis: await redis
35
36
.createClient({ url: env.REDIS_URL })
36
37
.on("error", (err) => {
+6
-6
apps/api/src/dropbox/app.ts
+6
-6
apps/api/src/dropbox/app.ts
···
55
55
client_id: env.DROPBOX_CLIENT_ID,
56
56
client_secret: env.DROPBOX_CLIENT_SECRET,
57
57
redirect_uri: env.DROPBOX_REDIRECT_URI,
58
-
}
58
+
},
59
59
);
60
60
61
61
const { dropbox, dropbox_tokens } = await ctx.db
···
64
64
.where(eq(tables.dropbox.userId, entries.state))
65
65
.leftJoin(
66
66
tables.dropboxTokens,
67
-
eq(tables.dropboxTokens.id, tables.dropbox.dropboxTokenId)
67
+
eq(tables.dropboxTokens.id, tables.dropbox.dropboxTokenId),
68
68
)
69
69
.limit(1)
70
70
.execute()
···
76
76
id: dropbox_tokens?.id,
77
77
refreshToken: encrypt(
78
78
response.data.refresh_token,
79
-
env.SPOTIFY_ENCRYPTION_KEY
79
+
env.SPOTIFY_ENCRYPTION_KEY,
80
80
),
81
81
})
82
82
.onConflictDoUpdate({
···
84
84
set: {
85
85
refreshToken: encrypt(
86
86
response.data.refresh_token,
87
-
env.SPOTIFY_ENCRYPTION_KEY
87
+
env.SPOTIFY_ENCRYPTION_KEY,
88
88
),
89
89
},
90
90
})
···
377
377
378
378
c.header(
379
379
"Content-Type",
380
-
response.headers["content-type"] || "application/octet-stream"
380
+
response.headers["content-type"] || "application/octet-stream",
381
381
);
382
382
c.header(
383
383
"Content-Disposition",
384
-
response.headers["content-disposition"] || "attachment"
384
+
response.headers["content-disposition"] || "attachment",
385
385
);
386
386
387
387
return new Response(response.data, {
+12
-12
apps/api/src/googledrive/app.ts
+12
-12
apps/api/src/googledrive/app.ts
···
42
42
}
43
43
44
44
const credentials = JSON.parse(
45
-
fs.readFileSync("credentials.json").toString("utf-8")
45
+
fs.readFileSync("credentials.json").toString("utf-8"),
46
46
);
47
47
const { client_id, client_secret } = credentials.installed || credentials.web;
48
48
const oAuth2Client = new google.auth.OAuth2(
49
49
client_id,
50
50
client_secret,
51
-
env.GOOGLE_REDIRECT_URI
51
+
env.GOOGLE_REDIRECT_URI,
52
52
);
53
53
54
54
// Generate Auth URL
···
70
70
const entries = Object.fromEntries(params.entries());
71
71
72
72
const credentials = JSON.parse(
73
-
fs.readFileSync("credentials.json").toString("utf-8")
73
+
fs.readFileSync("credentials.json").toString("utf-8"),
74
74
);
75
75
const { client_id, client_secret } = credentials.installed || credentials.web;
76
76
···
92
92
.innerJoin(users, eq(googleDrive.userId, users.id))
93
93
.leftJoin(
94
94
googleDriveTokens,
95
-
eq(googleDrive.googleDriveTokenId, googleDriveTokens.id)
95
+
eq(googleDrive.googleDriveTokenId, googleDriveTokens.id),
96
96
)
97
97
.where(eq(users.id, entries.state))
98
98
.limit(1)
···
105
105
.set({
106
106
refreshToken: encrypt(
107
107
response.data.refresh_token,
108
-
env.SPOTIFY_ENCRYPTION_KEY
108
+
env.SPOTIFY_ENCRYPTION_KEY,
109
109
),
110
110
})
111
111
.where(eq(googleDriveTokens.id, existingGoogleDrive.token.id))
···
117
117
.values({
118
118
refreshToken: encrypt(
119
119
response.data.refresh_token,
120
-
env.SPOTIFY_ENCRYPTION_KEY
120
+
env.SPOTIFY_ENCRYPTION_KEY,
121
121
),
122
122
})
123
123
.returning();
···
237
237
{
238
238
did,
239
239
parent_id,
240
-
}
240
+
},
241
241
);
242
242
return c.json(data);
243
243
}
···
258
258
{
259
259
did,
260
260
parent_id: response.data.files[0].id,
261
-
}
261
+
},
262
262
);
263
263
return c.json(data);
264
264
} catch (error) {
···
266
266
console.error("Axios error:", error.response?.data || error.message);
267
267
268
268
const credentials = JSON.parse(
269
-
fs.readFileSync("credentials.json").toString("utf-8")
269
+
fs.readFileSync("credentials.json").toString("utf-8"),
270
270
);
271
271
const { client_id, client_secret } =
272
272
credentials.installed || credentials.web;
273
273
const oAuth2Client = new google.auth.OAuth2(
274
274
client_id,
275
275
client_secret,
276
-
env.GOOGLE_REDIRECT_URI
276
+
env.GOOGLE_REDIRECT_URI,
277
277
);
278
278
279
279
// Generate Auth URL
···
362
362
363
363
c.header(
364
364
"Content-Type",
365
-
response.headers["content-type"] || "application/octet-stream"
365
+
response.headers["content-type"] || "application/octet-stream",
366
366
);
367
367
c.header(
368
368
"Content-Disposition",
369
-
response.headers["content-disposition"] || "attachment"
369
+
response.headers["content-disposition"] || "attachment",
370
370
);
371
371
372
372
return new Response(response.data, {
+3
-3
apps/api/src/index.ts
+3
-3
apps/api/src/index.ts
···
47
47
rateLimiter({
48
48
limit: 1000,
49
49
window: 30, // 👈 30 seconds
50
-
})
50
+
}),
51
51
);
52
52
53
53
app.use("*", async (c, next) => {
···
162
162
ctx.redis.get(`nowplaying:${user.did}:status`),
163
163
]);
164
164
return c.json(
165
-
nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {}
165
+
nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {},
166
166
);
167
167
});
168
168
···
314
314
listeners: 1,
315
315
sha256: item.track.sha256,
316
316
id: item.scrobble.id,
317
-
}))
317
+
})),
318
318
);
319
319
});
320
320
+110
apps/api/src/lexicon/index.ts
+110
apps/api/src/lexicon/index.ts
···
9
9
type StreamAuthVerifier,
10
10
} from "@atproto/xrpc-server";
11
11
import { schemas } from "./lexicons";
12
+
import type * as FmTealAlphaActorGetProfile from "./types/fm/teal/alpha/actor/getProfile";
13
+
import type * as FmTealAlphaActorGetProfiles from "./types/fm/teal/alpha/actor/getProfiles";
14
+
import type * as FmTealAlphaActorSearchActors from "./types/fm/teal/alpha/actor/searchActors";
15
+
import type * as FmTealAlphaFeedGetActorFeed from "./types/fm/teal/alpha/feed/getActorFeed";
16
+
import type * as FmTealAlphaFeedGetPlay from "./types/fm/teal/alpha/feed/getPlay";
12
17
import type * as AppRockskyActorGetActorAlbums from "./types/app/rocksky/actor/getActorAlbums";
13
18
import type * as AppRockskyActorGetActorArtists from "./types/app/rocksky/actor/getActorArtists";
14
19
import type * as AppRockskyActorGetActorLovedSongs from "./types/app/rocksky/actor/getActorLovedSongs";
···
90
95
91
96
export class Server {
92
97
xrpc: XrpcServer;
98
+
fm: FmNS;
93
99
app: AppNS;
94
100
com: ComNS;
95
101
96
102
constructor(options?: XrpcOptions) {
97
103
this.xrpc = createXrpcServer(schemas, options);
104
+
this.fm = new FmNS(this);
98
105
this.app = new AppNS(this);
99
106
this.com = new ComNS(this);
107
+
}
108
+
}
109
+
110
+
export class FmNS {
111
+
_server: Server;
112
+
teal: FmTealNS;
113
+
114
+
constructor(server: Server) {
115
+
this._server = server;
116
+
this.teal = new FmTealNS(server);
117
+
}
118
+
}
119
+
120
+
export class FmTealNS {
121
+
_server: Server;
122
+
alpha: FmTealAlphaNS;
123
+
124
+
constructor(server: Server) {
125
+
this._server = server;
126
+
this.alpha = new FmTealAlphaNS(server);
127
+
}
128
+
}
129
+
130
+
export class FmTealAlphaNS {
131
+
_server: Server;
132
+
actor: FmTealAlphaActorNS;
133
+
feed: FmTealAlphaFeedNS;
134
+
135
+
constructor(server: Server) {
136
+
this._server = server;
137
+
this.actor = new FmTealAlphaActorNS(server);
138
+
this.feed = new FmTealAlphaFeedNS(server);
139
+
}
140
+
}
141
+
142
+
export class FmTealAlphaActorNS {
143
+
_server: Server;
144
+
145
+
constructor(server: Server) {
146
+
this._server = server;
147
+
}
148
+
149
+
getProfile<AV extends AuthVerifier>(
150
+
cfg: ConfigOf<
151
+
AV,
152
+
FmTealAlphaActorGetProfile.Handler<ExtractAuth<AV>>,
153
+
FmTealAlphaActorGetProfile.HandlerReqCtx<ExtractAuth<AV>>
154
+
>,
155
+
) {
156
+
const nsid = "fm.teal.alpha.actor.getProfile"; // @ts-ignore
157
+
return this._server.xrpc.method(nsid, cfg);
158
+
}
159
+
160
+
getProfiles<AV extends AuthVerifier>(
161
+
cfg: ConfigOf<
162
+
AV,
163
+
FmTealAlphaActorGetProfiles.Handler<ExtractAuth<AV>>,
164
+
FmTealAlphaActorGetProfiles.HandlerReqCtx<ExtractAuth<AV>>
165
+
>,
166
+
) {
167
+
const nsid = "fm.teal.alpha.actor.getProfiles"; // @ts-ignore
168
+
return this._server.xrpc.method(nsid, cfg);
169
+
}
170
+
171
+
searchActors<AV extends AuthVerifier>(
172
+
cfg: ConfigOf<
173
+
AV,
174
+
FmTealAlphaActorSearchActors.Handler<ExtractAuth<AV>>,
175
+
FmTealAlphaActorSearchActors.HandlerReqCtx<ExtractAuth<AV>>
176
+
>,
177
+
) {
178
+
const nsid = "fm.teal.alpha.actor.searchActors"; // @ts-ignore
179
+
return this._server.xrpc.method(nsid, cfg);
180
+
}
181
+
}
182
+
183
+
export class FmTealAlphaFeedNS {
184
+
_server: Server;
185
+
186
+
constructor(server: Server) {
187
+
this._server = server;
188
+
}
189
+
190
+
getActorFeed<AV extends AuthVerifier>(
191
+
cfg: ConfigOf<
192
+
AV,
193
+
FmTealAlphaFeedGetActorFeed.Handler<ExtractAuth<AV>>,
194
+
FmTealAlphaFeedGetActorFeed.HandlerReqCtx<ExtractAuth<AV>>
195
+
>,
196
+
) {
197
+
const nsid = "fm.teal.alpha.feed.getActorFeed"; // @ts-ignore
198
+
return this._server.xrpc.method(nsid, cfg);
199
+
}
200
+
201
+
getPlay<AV extends AuthVerifier>(
202
+
cfg: ConfigOf<
203
+
AV,
204
+
FmTealAlphaFeedGetPlay.Handler<ExtractAuth<AV>>,
205
+
FmTealAlphaFeedGetPlay.HandlerReqCtx<ExtractAuth<AV>>
206
+
>,
207
+
) {
208
+
const nsid = "fm.teal.alpha.feed.getPlay"; // @ts-ignore
209
+
return this._server.xrpc.method(nsid, cfg);
100
210
}
101
211
}
102
212
+594
apps/api/src/lexicon/lexicons.ts
+594
apps/api/src/lexicon/lexicons.ts
···
4
4
import { type LexiconDoc, Lexicons } from "@atproto/lexicon";
5
5
6
6
export const schemaDict = {
7
+
FmTealAlphaActorDefs: {
8
+
lexicon: 1,
9
+
id: "fm.teal.alpha.actor.defs",
10
+
defs: {
11
+
profileView: {
12
+
type: "object",
13
+
properties: {
14
+
did: {
15
+
type: "string",
16
+
description: "The decentralized identifier of the actor",
17
+
},
18
+
displayName: {
19
+
type: "string",
20
+
},
21
+
description: {
22
+
type: "string",
23
+
description: "Free-form profile description text.",
24
+
},
25
+
descriptionFacets: {
26
+
type: "array",
27
+
description:
28
+
"Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon.",
29
+
items: {
30
+
type: "ref",
31
+
ref: "lex:app.bsky.richtext.facet",
32
+
},
33
+
},
34
+
featuredItem: {
35
+
type: "ref",
36
+
description:
37
+
"The user's most recent item featured on their profile.",
38
+
ref: "lex:fm.teal.alpha.actor.profile#featuredItem",
39
+
},
40
+
avatar: {
41
+
type: "string",
42
+
description: "IPLD of the avatar",
43
+
},
44
+
banner: {
45
+
type: "string",
46
+
description: "IPLD of the banner image",
47
+
},
48
+
createdAt: {
49
+
type: "string",
50
+
format: "datetime",
51
+
},
52
+
},
53
+
},
54
+
miniProfileView: {
55
+
type: "object",
56
+
properties: {
57
+
did: {
58
+
type: "string",
59
+
description: "The decentralized identifier of the actor",
60
+
},
61
+
displayName: {
62
+
type: "string",
63
+
},
64
+
handle: {
65
+
type: "string",
66
+
},
67
+
avatar: {
68
+
type: "string",
69
+
description: "IPLD of the avatar",
70
+
},
71
+
},
72
+
},
73
+
},
74
+
},
75
+
FmTealAlphaActorGetProfile: {
76
+
lexicon: 1,
77
+
id: "fm.teal.alpha.actor.getProfile",
78
+
description:
79
+
"This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.",
80
+
defs: {
81
+
main: {
82
+
type: "query",
83
+
parameters: {
84
+
type: "params",
85
+
required: ["actor"],
86
+
properties: {
87
+
actor: {
88
+
type: "string",
89
+
format: "at-identifier",
90
+
description: "The author's DID",
91
+
},
92
+
},
93
+
},
94
+
output: {
95
+
encoding: "application/json",
96
+
schema: {
97
+
type: "object",
98
+
required: ["actor"],
99
+
properties: {
100
+
actor: {
101
+
type: "ref",
102
+
ref: "lex:fm.teal.alpha.actor.defs#profileView",
103
+
},
104
+
},
105
+
},
106
+
},
107
+
},
108
+
},
109
+
},
110
+
FmTealAlphaActorGetProfiles: {
111
+
lexicon: 1,
112
+
id: "fm.teal.alpha.actor.getProfiles",
113
+
description:
114
+
"This lexicon is in a not officially released state. It is subject to change. | Retrieves the associated profile.",
115
+
defs: {
116
+
main: {
117
+
type: "query",
118
+
parameters: {
119
+
type: "params",
120
+
required: ["actors"],
121
+
properties: {
122
+
actors: {
123
+
type: "array",
124
+
items: {
125
+
type: "string",
126
+
format: "at-identifier",
127
+
},
128
+
description: "Array of actor DIDs",
129
+
},
130
+
},
131
+
},
132
+
output: {
133
+
encoding: "application/json",
134
+
schema: {
135
+
type: "object",
136
+
required: ["actors"],
137
+
properties: {
138
+
actors: {
139
+
type: "array",
140
+
items: {
141
+
type: "ref",
142
+
ref: "lex:fm.teal.alpha.actor.defs#miniProfileView",
143
+
},
144
+
},
145
+
},
146
+
},
147
+
},
148
+
},
149
+
},
150
+
},
151
+
FmTealAlphaActorProfile: {
152
+
lexicon: 1,
153
+
id: "fm.teal.alpha.actor.profile",
154
+
defs: {
155
+
main: {
156
+
type: "record",
157
+
description:
158
+
"This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm account profile.",
159
+
key: "literal:self",
160
+
record: {
161
+
type: "object",
162
+
properties: {
163
+
displayName: {
164
+
type: "string",
165
+
maxGraphemes: 64,
166
+
maxLength: 640,
167
+
},
168
+
description: {
169
+
type: "string",
170
+
description: "Free-form profile description text.",
171
+
maxGraphemes: 256,
172
+
maxLength: 2560,
173
+
},
174
+
descriptionFacets: {
175
+
type: "array",
176
+
description:
177
+
"Annotations of text in the profile description (mentions, URLs, hashtags, etc).",
178
+
items: {
179
+
type: "ref",
180
+
ref: "lex:app.bsky.richtext.facet",
181
+
},
182
+
},
183
+
featuredItem: {
184
+
type: "ref",
185
+
description:
186
+
"The user's most recent item featured on their profile.",
187
+
ref: "lex:fm.teal.alpha.actor.profile#featuredItem",
188
+
},
189
+
avatar: {
190
+
type: "blob",
191
+
description:
192
+
"Small image to be displayed next to posts from account. AKA, 'profile picture'",
193
+
accept: ["image/png", "image/jpeg"],
194
+
maxSize: 1000000,
195
+
},
196
+
banner: {
197
+
type: "blob",
198
+
description:
199
+
"Larger horizontal image to display behind profile view.",
200
+
accept: ["image/png", "image/jpeg"],
201
+
maxSize: 1000000,
202
+
},
203
+
createdAt: {
204
+
type: "string",
205
+
format: "datetime",
206
+
},
207
+
},
208
+
},
209
+
},
210
+
featuredItem: {
211
+
type: "object",
212
+
required: ["mbid", "type"],
213
+
properties: {
214
+
mbid: {
215
+
type: "string",
216
+
description: "The Musicbrainz ID of the item",
217
+
},
218
+
type: {
219
+
type: "string",
220
+
description:
221
+
"The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc.",
222
+
},
223
+
},
224
+
},
225
+
},
226
+
},
227
+
FmTealAlphaActorSearchActors: {
228
+
lexicon: 1,
229
+
id: "fm.teal.alpha.actor.searchActors",
230
+
description:
231
+
"This lexicon is in a not officially released state. It is subject to change. | Searches for actors based on profile contents.",
232
+
defs: {
233
+
main: {
234
+
type: "query",
235
+
parameters: {
236
+
type: "params",
237
+
required: ["q"],
238
+
properties: {
239
+
q: {
240
+
type: "string",
241
+
description: "The search query",
242
+
maxGraphemes: 128,
243
+
maxLength: 640,
244
+
},
245
+
limit: {
246
+
type: "integer",
247
+
description: "The maximum number of actors to return",
248
+
minimum: 1,
249
+
maximum: 25,
250
+
},
251
+
cursor: {
252
+
type: "string",
253
+
description: "Cursor for pagination",
254
+
},
255
+
},
256
+
},
257
+
output: {
258
+
encoding: "application/json",
259
+
schema: {
260
+
type: "object",
261
+
required: ["actors"],
262
+
properties: {
263
+
actors: {
264
+
type: "array",
265
+
items: {
266
+
type: "ref",
267
+
ref: "lex:fm.teal.alpha.actor.defs#miniProfileView",
268
+
},
269
+
},
270
+
cursor: {
271
+
type: "string",
272
+
description: "Cursor for pagination",
273
+
},
274
+
},
275
+
},
276
+
},
277
+
},
278
+
},
279
+
},
280
+
FmTealAlphaActorStatus: {
281
+
lexicon: 1,
282
+
id: "fm.teal.alpha.actor.status",
283
+
defs: {
284
+
main: {
285
+
type: "record",
286
+
description:
287
+
"This lexicon is in a not officially released state. It is subject to change. | A declaration of the status of the actor. Only one can be shown at a time. If there are multiple, the latest record should be picked and earlier records should be deleted or tombstoned.",
288
+
key: "literal:self",
289
+
record: {
290
+
type: "object",
291
+
required: ["time", "item"],
292
+
properties: {
293
+
time: {
294
+
type: "string",
295
+
format: "datetime",
296
+
description: "The unix timestamp of when the item was recorded",
297
+
},
298
+
expiry: {
299
+
type: "string",
300
+
format: "datetime",
301
+
description:
302
+
"The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time.",
303
+
},
304
+
item: {
305
+
type: "ref",
306
+
ref: "lex:fm.teal.alpha.feed.defs#playView",
307
+
},
308
+
},
309
+
},
310
+
},
311
+
},
312
+
},
313
+
FmTealAlphaFeedDefs: {
314
+
lexicon: 1,
315
+
id: "fm.teal.alpha.feed.defs",
316
+
description:
317
+
"This lexicon is in a not officially released state. It is subject to change. | Misc. items related to feeds.",
318
+
defs: {
319
+
playView: {
320
+
type: "object",
321
+
required: ["trackName", "artists"],
322
+
properties: {
323
+
trackName: {
324
+
type: "string",
325
+
minLength: 1,
326
+
maxLength: 256,
327
+
maxGraphemes: 2560,
328
+
description: "The name of the track",
329
+
},
330
+
trackMbId: {
331
+
type: "string",
332
+
description: "The Musicbrainz ID of the track",
333
+
},
334
+
recordingMbId: {
335
+
type: "string",
336
+
description: "The Musicbrainz recording ID of the track",
337
+
},
338
+
duration: {
339
+
type: "integer",
340
+
description: "The length of the track in seconds",
341
+
},
342
+
artists: {
343
+
type: "array",
344
+
items: {
345
+
type: "ref",
346
+
ref: "lex:fm.teal.alpha.feed.defs#artist",
347
+
},
348
+
description: "Array of artists in order of original appearance.",
349
+
},
350
+
releaseName: {
351
+
type: "string",
352
+
maxLength: 256,
353
+
maxGraphemes: 2560,
354
+
description: "The name of the release/album",
355
+
},
356
+
releaseMbId: {
357
+
type: "string",
358
+
description: "The Musicbrainz release ID",
359
+
},
360
+
isrc: {
361
+
type: "string",
362
+
description: "The ISRC code associated with the recording",
363
+
},
364
+
originUrl: {
365
+
type: "string",
366
+
description: "The URL associated with this track",
367
+
},
368
+
musicServiceBaseDomain: {
369
+
type: "string",
370
+
description:
371
+
"The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided.",
372
+
},
373
+
submissionClientAgent: {
374
+
type: "string",
375
+
maxLength: 256,
376
+
maxGraphemes: 2560,
377
+
description:
378
+
"A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided.",
379
+
},
380
+
playedTime: {
381
+
type: "string",
382
+
format: "datetime",
383
+
description: "The unix timestamp of when the track was played",
384
+
},
385
+
},
386
+
},
387
+
artist: {
388
+
type: "object",
389
+
required: ["artistName"],
390
+
properties: {
391
+
artistName: {
392
+
type: "string",
393
+
minLength: 1,
394
+
maxLength: 256,
395
+
maxGraphemes: 2560,
396
+
description: "The name of the artist",
397
+
},
398
+
artistMbId: {
399
+
type: "string",
400
+
description: "The Musicbrainz ID of the artist",
401
+
},
402
+
},
403
+
},
404
+
},
405
+
},
406
+
FmTealAlphaFeedGetActorFeed: {
407
+
lexicon: 1,
408
+
id: "fm.teal.alpha.feed.getActorFeed",
409
+
description:
410
+
"This lexicon is in a not officially released state. It is subject to change. | Retrieves multiple plays from the index or via an author's DID.",
411
+
defs: {
412
+
main: {
413
+
type: "query",
414
+
parameters: {
415
+
type: "params",
416
+
required: ["authorDID"],
417
+
properties: {
418
+
authorDID: {
419
+
type: "string",
420
+
format: "at-identifier",
421
+
description: "The author's DID for the play",
422
+
},
423
+
cursor: {
424
+
type: "string",
425
+
description: "The cursor to start the query from",
426
+
},
427
+
limit: {
428
+
type: "integer",
429
+
description:
430
+
"The upper limit of tracks to get per request. Default is 20, max is 50.",
431
+
},
432
+
},
433
+
},
434
+
output: {
435
+
encoding: "application/json",
436
+
schema: {
437
+
type: "object",
438
+
required: ["plays"],
439
+
properties: {
440
+
plays: {
441
+
type: "array",
442
+
items: {
443
+
type: "ref",
444
+
ref: "lex:fm.teal.alpha.feed.defs#playView",
445
+
},
446
+
},
447
+
},
448
+
},
449
+
},
450
+
},
451
+
},
452
+
},
453
+
FmTealAlphaFeedGetPlay: {
454
+
lexicon: 1,
455
+
id: "fm.teal.alpha.feed.getPlay",
456
+
description:
457
+
"This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.",
458
+
defs: {
459
+
main: {
460
+
type: "query",
461
+
parameters: {
462
+
type: "params",
463
+
required: ["authorDID", "rkey"],
464
+
properties: {
465
+
authorDID: {
466
+
type: "string",
467
+
format: "at-identifier",
468
+
description: "The author's DID for the play",
469
+
},
470
+
rkey: {
471
+
type: "string",
472
+
description: "The record key of the play",
473
+
},
474
+
},
475
+
},
476
+
output: {
477
+
encoding: "application/json",
478
+
schema: {
479
+
type: "object",
480
+
required: ["play"],
481
+
properties: {
482
+
play: {
483
+
type: "ref",
484
+
ref: "lex:fm.teal.alpha.feed.defs#playView",
485
+
},
486
+
},
487
+
},
488
+
},
489
+
},
490
+
},
491
+
},
492
+
FmTealAlphaFeedPlay: {
493
+
lexicon: 1,
494
+
id: "fm.teal.alpha.feed.play",
495
+
description:
496
+
"This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm play. Plays are submitted as a result of a user listening to a track. Plays should be marked as tracked when a user has listened to the entire track if it's under 2 minutes long, or half of the track's duration up to 4 minutes, whichever is longest.",
497
+
defs: {
498
+
main: {
499
+
type: "record",
500
+
key: "tid",
501
+
record: {
502
+
type: "object",
503
+
required: ["trackName"],
504
+
properties: {
505
+
trackName: {
506
+
type: "string",
507
+
minLength: 1,
508
+
maxLength: 256,
509
+
maxGraphemes: 2560,
510
+
description: "The name of the track",
511
+
},
512
+
trackMbId: {
513
+
type: "string",
514
+
description: "The Musicbrainz ID of the track",
515
+
},
516
+
recordingMbId: {
517
+
type: "string",
518
+
description: "The Musicbrainz recording ID of the track",
519
+
},
520
+
duration: {
521
+
type: "integer",
522
+
description: "The length of the track in seconds",
523
+
},
524
+
artistNames: {
525
+
type: "array",
526
+
items: {
527
+
type: "string",
528
+
minLength: 1,
529
+
maxLength: 256,
530
+
maxGraphemes: 2560,
531
+
},
532
+
description:
533
+
"Array of artist names in order of original appearance. Prefer using 'artists'.",
534
+
},
535
+
artistMbIds: {
536
+
type: "array",
537
+
items: {
538
+
type: "string",
539
+
},
540
+
description:
541
+
"Array of Musicbrainz artist IDs. Prefer using 'artists'.",
542
+
},
543
+
artists: {
544
+
type: "array",
545
+
items: {
546
+
type: "ref",
547
+
ref: "lex:fm.teal.alpha.feed.defs#artist",
548
+
},
549
+
description: "Array of artists in order of original appearance.",
550
+
},
551
+
releaseName: {
552
+
type: "string",
553
+
maxLength: 256,
554
+
maxGraphemes: 2560,
555
+
description: "The name of the release/album",
556
+
},
557
+
releaseMbId: {
558
+
type: "string",
559
+
description: "The Musicbrainz release ID",
560
+
},
561
+
isrc: {
562
+
type: "string",
563
+
description: "The ISRC code associated with the recording",
564
+
},
565
+
originUrl: {
566
+
type: "string",
567
+
description: "The URL associated with this track",
568
+
},
569
+
musicServiceBaseDomain: {
570
+
type: "string",
571
+
description:
572
+
"The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided.",
573
+
},
574
+
submissionClientAgent: {
575
+
type: "string",
576
+
maxLength: 256,
577
+
maxGraphemes: 2560,
578
+
description:
579
+
"A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided.",
580
+
},
581
+
playedTime: {
582
+
type: "string",
583
+
format: "datetime",
584
+
description: "The unix timestamp of when the track was played",
585
+
},
586
+
},
587
+
},
588
+
},
589
+
},
590
+
},
7
591
AppRockskyActorDefs: {
8
592
lexicon: 1,
9
593
id: "app.rocksky.actor.defs",
···
4421
5005
export const schemas = Object.values(schemaDict);
4422
5006
export const lexicons: Lexicons = new Lexicons(schemas);
4423
5007
export const ids = {
5008
+
FmTealAlphaActorDefs: "fm.teal.alpha.actor.defs",
5009
+
FmTealAlphaActorGetProfile: "fm.teal.alpha.actor.getProfile",
5010
+
FmTealAlphaActorGetProfiles: "fm.teal.alpha.actor.getProfiles",
5011
+
FmTealAlphaActorProfile: "fm.teal.alpha.actor.profile",
5012
+
FmTealAlphaActorSearchActors: "fm.teal.alpha.actor.searchActors",
5013
+
FmTealAlphaActorStatus: "fm.teal.alpha.actor.status",
5014
+
FmTealAlphaFeedDefs: "fm.teal.alpha.feed.defs",
5015
+
FmTealAlphaFeedGetActorFeed: "fm.teal.alpha.feed.getActorFeed",
5016
+
FmTealAlphaFeedGetPlay: "fm.teal.alpha.feed.getPlay",
5017
+
FmTealAlphaFeedPlay: "fm.teal.alpha.feed.play",
4424
5018
AppRockskyActorDefs: "app.rocksky.actor.defs",
4425
5019
AppRockskyActorGetActorAlbums: "app.rocksky.actor.getActorAlbums",
4426
5020
AppRockskyActorGetActorArtists: "app.rocksky.actor.getActorArtists",
+60
apps/api/src/lexicon/types/fm/teal/alpha/actor/defs.ts
+60
apps/api/src/lexicon/types/fm/teal/alpha/actor/defs.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 AppBskyRichtextFacet from "../../../../app/bsky/richtext/facet";
9
+
import type * as FmTealAlphaActorProfile from "./profile";
10
+
11
+
export interface ProfileView {
12
+
/** The decentralized identifier of the actor */
13
+
did?: string;
14
+
displayName?: string;
15
+
/** Free-form profile description text. */
16
+
description?: string;
17
+
/** Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon. */
18
+
descriptionFacets?: AppBskyRichtextFacet.Main[];
19
+
featuredItem?: FmTealAlphaActorProfile.FeaturedItem;
20
+
/** IPLD of the avatar */
21
+
avatar?: string;
22
+
/** IPLD of the banner image */
23
+
banner?: string;
24
+
createdAt?: string;
25
+
[k: string]: unknown;
26
+
}
27
+
28
+
export function isProfileView(v: unknown): v is ProfileView {
29
+
return (
30
+
isObj(v) &&
31
+
hasProp(v, "$type") &&
32
+
v.$type === "fm.teal.alpha.actor.defs#profileView"
33
+
);
34
+
}
35
+
36
+
export function validateProfileView(v: unknown): ValidationResult {
37
+
return lexicons.validate("fm.teal.alpha.actor.defs#profileView", v);
38
+
}
39
+
40
+
export interface MiniProfileView {
41
+
/** The decentralized identifier of the actor */
42
+
did?: string;
43
+
displayName?: string;
44
+
handle?: string;
45
+
/** IPLD of the avatar */
46
+
avatar?: string;
47
+
[k: string]: unknown;
48
+
}
49
+
50
+
export function isMiniProfileView(v: unknown): v is MiniProfileView {
51
+
return (
52
+
isObj(v) &&
53
+
hasProp(v, "$type") &&
54
+
v.$type === "fm.teal.alpha.actor.defs#miniProfileView"
55
+
);
56
+
}
57
+
58
+
export function validateMiniProfileView(v: unknown): ValidationResult {
59
+
return lexicons.validate("fm.teal.alpha.actor.defs#miniProfileView", v);
60
+
}
+48
apps/api/src/lexicon/types/fm/teal/alpha/actor/getProfile.ts
+48
apps/api/src/lexicon/types/fm/teal/alpha/actor/getProfile.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import type express from "express";
5
+
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
+
import { lexicons } from "../../../../../lexicons";
7
+
import { isObj, hasProp } from "../../../../../util";
8
+
import { CID } from "multiformats/cid";
9
+
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
+
import type * as FmTealAlphaActorDefs from "./defs";
11
+
12
+
export interface QueryParams {
13
+
/** The author's DID */
14
+
actor: string;
15
+
}
16
+
17
+
export type InputSchema = undefined;
18
+
19
+
export interface OutputSchema {
20
+
actor: FmTealAlphaActorDefs.ProfileView;
21
+
[k: string]: unknown;
22
+
}
23
+
24
+
export type HandlerInput = undefined;
25
+
26
+
export interface HandlerSuccess {
27
+
encoding: "application/json";
28
+
body: OutputSchema;
29
+
headers?: { [key: string]: string };
30
+
}
31
+
32
+
export interface HandlerError {
33
+
status: number;
34
+
message?: string;
35
+
}
36
+
37
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
38
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
39
+
auth: HA;
40
+
params: QueryParams;
41
+
input: HandlerInput;
42
+
req: express.Request;
43
+
res: express.Response;
44
+
resetRouteRateLimits: () => Promise<void>;
45
+
};
46
+
export type Handler<HA extends HandlerAuth = never> = (
47
+
ctx: HandlerReqCtx<HA>,
48
+
) => Promise<HandlerOutput> | HandlerOutput;
+48
apps/api/src/lexicon/types/fm/teal/alpha/actor/getProfiles.ts
+48
apps/api/src/lexicon/types/fm/teal/alpha/actor/getProfiles.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import type express from "express";
5
+
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
+
import { lexicons } from "../../../../../lexicons";
7
+
import { isObj, hasProp } from "../../../../../util";
8
+
import { CID } from "multiformats/cid";
9
+
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
+
import type * as FmTealAlphaActorDefs from "./defs";
11
+
12
+
export interface QueryParams {
13
+
/** Array of actor DIDs */
14
+
actors: string[];
15
+
}
16
+
17
+
export type InputSchema = undefined;
18
+
19
+
export interface OutputSchema {
20
+
actors: FmTealAlphaActorDefs.MiniProfileView[];
21
+
[k: string]: unknown;
22
+
}
23
+
24
+
export type HandlerInput = undefined;
25
+
26
+
export interface HandlerSuccess {
27
+
encoding: "application/json";
28
+
body: OutputSchema;
29
+
headers?: { [key: string]: string };
30
+
}
31
+
32
+
export interface HandlerError {
33
+
status: number;
34
+
message?: string;
35
+
}
36
+
37
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
38
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
39
+
auth: HA;
40
+
params: QueryParams;
41
+
input: HandlerInput;
42
+
req: express.Request;
43
+
res: express.Response;
44
+
resetRouteRateLimits: () => Promise<void>;
45
+
};
46
+
export type Handler<HA extends HandlerAuth = never> = (
47
+
ctx: HandlerReqCtx<HA>,
48
+
) => Promise<HandlerOutput> | HandlerOutput;
+56
apps/api/src/lexicon/types/fm/teal/alpha/actor/profile.ts
+56
apps/api/src/lexicon/types/fm/teal/alpha/actor/profile.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 AppBskyRichtextFacet from "../../../../app/bsky/richtext/facet";
9
+
10
+
export interface Record {
11
+
displayName?: string;
12
+
/** Free-form profile description text. */
13
+
description?: string;
14
+
/** Annotations of text in the profile description (mentions, URLs, hashtags, etc). */
15
+
descriptionFacets?: AppBskyRichtextFacet.Main[];
16
+
featuredItem?: FeaturedItem;
17
+
/** Small image to be displayed next to posts from account. AKA, 'profile picture' */
18
+
avatar?: BlobRef;
19
+
/** Larger horizontal image to display behind profile view. */
20
+
banner?: BlobRef;
21
+
createdAt?: string;
22
+
[k: string]: unknown;
23
+
}
24
+
25
+
export function isRecord(v: unknown): v is Record {
26
+
return (
27
+
isObj(v) &&
28
+
hasProp(v, "$type") &&
29
+
(v.$type === "fm.teal.alpha.actor.profile#main" ||
30
+
v.$type === "fm.teal.alpha.actor.profile")
31
+
);
32
+
}
33
+
34
+
export function validateRecord(v: unknown): ValidationResult {
35
+
return lexicons.validate("fm.teal.alpha.actor.profile#main", v);
36
+
}
37
+
38
+
export interface FeaturedItem {
39
+
/** The Musicbrainz ID of the item */
40
+
mbid: string;
41
+
/** The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc. */
42
+
type: string;
43
+
[k: string]: unknown;
44
+
}
45
+
46
+
export function isFeaturedItem(v: unknown): v is FeaturedItem {
47
+
return (
48
+
isObj(v) &&
49
+
hasProp(v, "$type") &&
50
+
v.$type === "fm.teal.alpha.actor.profile#featuredItem"
51
+
);
52
+
}
53
+
54
+
export function validateFeaturedItem(v: unknown): ValidationResult {
55
+
return lexicons.validate("fm.teal.alpha.actor.profile#featuredItem", v);
56
+
}
+54
apps/api/src/lexicon/types/fm/teal/alpha/actor/searchActors.ts
+54
apps/api/src/lexicon/types/fm/teal/alpha/actor/searchActors.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import type express from "express";
5
+
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
+
import { lexicons } from "../../../../../lexicons";
7
+
import { isObj, hasProp } from "../../../../../util";
8
+
import { CID } from "multiformats/cid";
9
+
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
+
import type * as FmTealAlphaActorDefs from "./defs";
11
+
12
+
export interface QueryParams {
13
+
/** The search query */
14
+
q: string;
15
+
/** The maximum number of actors to return */
16
+
limit?: number;
17
+
/** Cursor for pagination */
18
+
cursor?: string;
19
+
}
20
+
21
+
export type InputSchema = undefined;
22
+
23
+
export interface OutputSchema {
24
+
actors: FmTealAlphaActorDefs.MiniProfileView[];
25
+
/** Cursor for pagination */
26
+
cursor?: string;
27
+
[k: string]: unknown;
28
+
}
29
+
30
+
export type HandlerInput = undefined;
31
+
32
+
export interface HandlerSuccess {
33
+
encoding: "application/json";
34
+
body: OutputSchema;
35
+
headers?: { [key: string]: string };
36
+
}
37
+
38
+
export interface HandlerError {
39
+
status: number;
40
+
message?: string;
41
+
}
42
+
43
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
44
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
45
+
auth: HA;
46
+
params: QueryParams;
47
+
input: HandlerInput;
48
+
req: express.Request;
49
+
res: express.Response;
50
+
resetRouteRateLimits: () => Promise<void>;
51
+
};
52
+
export type Handler<HA extends HandlerAuth = never> = (
53
+
ctx: HandlerReqCtx<HA>,
54
+
) => Promise<HandlerOutput> | HandlerOutput;
+30
apps/api/src/lexicon/types/fm/teal/alpha/actor/status.ts
+30
apps/api/src/lexicon/types/fm/teal/alpha/actor/status.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 FmTealAlphaFeedDefs from "../feed/defs";
9
+
10
+
export interface Record {
11
+
/** The unix timestamp of when the item was recorded */
12
+
time: string;
13
+
/** The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time. */
14
+
expiry?: string;
15
+
item: FmTealAlphaFeedDefs.PlayView;
16
+
[k: string]: unknown;
17
+
}
18
+
19
+
export function isRecord(v: unknown): v is Record {
20
+
return (
21
+
isObj(v) &&
22
+
hasProp(v, "$type") &&
23
+
(v.$type === "fm.teal.alpha.actor.status#main" ||
24
+
v.$type === "fm.teal.alpha.actor.status")
25
+
);
26
+
}
27
+
28
+
export function validateRecord(v: unknown): ValidationResult {
29
+
return lexicons.validate("fm.teal.alpha.actor.status#main", v);
30
+
}
+67
apps/api/src/lexicon/types/fm/teal/alpha/feed/defs.ts
+67
apps/api/src/lexicon/types/fm/teal/alpha/feed/defs.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
+
9
+
export interface PlayView {
10
+
/** The name of the track */
11
+
trackName: string;
12
+
/** The Musicbrainz ID of the track */
13
+
trackMbId?: string;
14
+
/** The Musicbrainz recording ID of the track */
15
+
recordingMbId?: string;
16
+
/** The length of the track in seconds */
17
+
duration?: number;
18
+
/** Array of artists in order of original appearance. */
19
+
artists: Artist[];
20
+
/** The name of the release/album */
21
+
releaseName?: string;
22
+
/** The Musicbrainz release ID */
23
+
releaseMbId?: string;
24
+
/** The ISRC code associated with the recording */
25
+
isrc?: string;
26
+
/** The URL associated with this track */
27
+
originUrl?: string;
28
+
/** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided. */
29
+
musicServiceBaseDomain?: string;
30
+
/** A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided. */
31
+
submissionClientAgent?: string;
32
+
/** The unix timestamp of when the track was played */
33
+
playedTime?: string;
34
+
[k: string]: unknown;
35
+
}
36
+
37
+
export function isPlayView(v: unknown): v is PlayView {
38
+
return (
39
+
isObj(v) &&
40
+
hasProp(v, "$type") &&
41
+
v.$type === "fm.teal.alpha.feed.defs#playView"
42
+
);
43
+
}
44
+
45
+
export function validatePlayView(v: unknown): ValidationResult {
46
+
return lexicons.validate("fm.teal.alpha.feed.defs#playView", v);
47
+
}
48
+
49
+
export interface Artist {
50
+
/** The name of the artist */
51
+
artistName: string;
52
+
/** The Musicbrainz ID of the artist */
53
+
artistMbId?: string;
54
+
[k: string]: unknown;
55
+
}
56
+
57
+
export function isArtist(v: unknown): v is Artist {
58
+
return (
59
+
isObj(v) &&
60
+
hasProp(v, "$type") &&
61
+
v.$type === "fm.teal.alpha.feed.defs#artist"
62
+
);
63
+
}
64
+
65
+
export function validateArtist(v: unknown): ValidationResult {
66
+
return lexicons.validate("fm.teal.alpha.feed.defs#artist", v);
67
+
}
+52
apps/api/src/lexicon/types/fm/teal/alpha/feed/getActorFeed.ts
+52
apps/api/src/lexicon/types/fm/teal/alpha/feed/getActorFeed.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import type express from "express";
5
+
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
+
import { lexicons } from "../../../../../lexicons";
7
+
import { isObj, hasProp } from "../../../../../util";
8
+
import { CID } from "multiformats/cid";
9
+
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
+
import type * as FmTealAlphaFeedDefs from "./defs";
11
+
12
+
export interface QueryParams {
13
+
/** The author's DID for the play */
14
+
authorDID: string;
15
+
/** The cursor to start the query from */
16
+
cursor?: string;
17
+
/** The upper limit of tracks to get per request. Default is 20, max is 50. */
18
+
limit?: number;
19
+
}
20
+
21
+
export type InputSchema = undefined;
22
+
23
+
export interface OutputSchema {
24
+
plays: FmTealAlphaFeedDefs.PlayView[];
25
+
[k: string]: unknown;
26
+
}
27
+
28
+
export type HandlerInput = undefined;
29
+
30
+
export interface HandlerSuccess {
31
+
encoding: "application/json";
32
+
body: OutputSchema;
33
+
headers?: { [key: string]: string };
34
+
}
35
+
36
+
export interface HandlerError {
37
+
status: number;
38
+
message?: string;
39
+
}
40
+
41
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
42
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
43
+
auth: HA;
44
+
params: QueryParams;
45
+
input: HandlerInput;
46
+
req: express.Request;
47
+
res: express.Response;
48
+
resetRouteRateLimits: () => Promise<void>;
49
+
};
50
+
export type Handler<HA extends HandlerAuth = never> = (
51
+
ctx: HandlerReqCtx<HA>,
52
+
) => Promise<HandlerOutput> | HandlerOutput;
+50
apps/api/src/lexicon/types/fm/teal/alpha/feed/getPlay.ts
+50
apps/api/src/lexicon/types/fm/teal/alpha/feed/getPlay.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import type express from "express";
5
+
import { ValidationResult, BlobRef } from "@atproto/lexicon";
6
+
import { lexicons } from "../../../../../lexicons";
7
+
import { isObj, hasProp } from "../../../../../util";
8
+
import { CID } from "multiformats/cid";
9
+
import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server";
10
+
import type * as FmTealAlphaFeedDefs from "./defs";
11
+
12
+
export interface QueryParams {
13
+
/** The author's DID for the play */
14
+
authorDID: string;
15
+
/** The record key of the play */
16
+
rkey: string;
17
+
}
18
+
19
+
export type InputSchema = undefined;
20
+
21
+
export interface OutputSchema {
22
+
play: FmTealAlphaFeedDefs.PlayView;
23
+
[k: string]: unknown;
24
+
}
25
+
26
+
export type HandlerInput = undefined;
27
+
28
+
export interface HandlerSuccess {
29
+
encoding: "application/json";
30
+
body: OutputSchema;
31
+
headers?: { [key: string]: string };
32
+
}
33
+
34
+
export interface HandlerError {
35
+
status: number;
36
+
message?: string;
37
+
}
38
+
39
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
40
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
41
+
auth: HA;
42
+
params: QueryParams;
43
+
input: HandlerInput;
44
+
req: express.Request;
45
+
res: express.Response;
46
+
resetRouteRateLimits: () => Promise<void>;
47
+
};
48
+
export type Handler<HA extends HandlerAuth = never> = (
49
+
ctx: HandlerReqCtx<HA>,
50
+
) => Promise<HandlerOutput> | HandlerOutput;
+53
apps/api/src/lexicon/types/fm/teal/alpha/feed/play.ts
+53
apps/api/src/lexicon/types/fm/teal/alpha/feed/play.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 FmTealAlphaFeedDefs from "./defs";
9
+
10
+
export interface Record {
11
+
/** The name of the track */
12
+
trackName: string;
13
+
/** The Musicbrainz ID of the track */
14
+
trackMbId?: string;
15
+
/** The Musicbrainz recording ID of the track */
16
+
recordingMbId?: string;
17
+
/** The length of the track in seconds */
18
+
duration?: number;
19
+
/** Array of artist names in order of original appearance. Prefer using 'artists'. */
20
+
artistNames?: string[];
21
+
/** Array of Musicbrainz artist IDs. Prefer using 'artists'. */
22
+
artistMbIds?: string[];
23
+
/** Array of artists in order of original appearance. */
24
+
artists?: FmTealAlphaFeedDefs.Artist[];
25
+
/** The name of the release/album */
26
+
releaseName?: string;
27
+
/** The Musicbrainz release ID */
28
+
releaseMbId?: string;
29
+
/** The ISRC code associated with the recording */
30
+
isrc?: string;
31
+
/** The URL associated with this track */
32
+
originUrl?: string;
33
+
/** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided. */
34
+
musicServiceBaseDomain?: string;
35
+
/** A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided. */
36
+
submissionClientAgent?: string;
37
+
/** The unix timestamp of when the track was played */
38
+
playedTime?: string;
39
+
[k: string]: unknown;
40
+
}
41
+
42
+
export function isRecord(v: unknown): v is Record {
43
+
return (
44
+
isObj(v) &&
45
+
hasProp(v, "$type") &&
46
+
(v.$type === "fm.teal.alpha.feed.play#main" ||
47
+
v.$type === "fm.teal.alpha.feed.play")
48
+
);
49
+
}
50
+
51
+
export function validateRecord(v: unknown): ValidationResult {
52
+
return lexicons.validate("fm.teal.alpha.feed.play#main", v);
53
+
}
+1
apps/api/src/lib/env.ts
+1
apps/api/src/lib/env.ts
···
35
35
DROPBOX: str({ default: "http://localhost:7881" }),
36
36
TRACKLIST: str({ default: "http://localhost:7884" }),
37
37
REDIS_URL: str({ default: "redis://localhost:6379" }),
38
+
MUSICBRAINZ_URL: str({ devDefault: "http://localhost:8088" }),
38
39
PRIVATE_KEY_1: str({}),
39
40
PRIVATE_KEY_2: str({}),
40
41
PRIVATE_KEY_3: str({}),
+10
-10
apps/api/src/lovedtracks/lovedtracks.service.ts
+10
-10
apps/api/src/lovedtracks/lovedtracks.service.ts
···
18
18
ctx: Context,
19
19
track: Track,
20
20
user,
21
-
agent: Agent
21
+
agent: Agent,
22
22
) {
23
23
const trackSha256 = createHash("sha256")
24
24
.update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase())
···
156
156
.select()
157
157
.from(albumTracks)
158
158
.where(
159
-
and(eq(albumTracks.albumId, albumId), eq(albumTracks.trackId, trackId))
159
+
and(eq(albumTracks.albumId, albumId), eq(albumTracks.trackId, trackId)),
160
160
)
161
161
.limit(1)
162
162
.then((rows) => rows[0]);
···
175
175
.where(
176
176
and(
177
177
eq(artistTracks.artistId, artistId),
178
-
eq(artistTracks.trackId, trackId)
179
-
)
178
+
eq(artistTracks.trackId, trackId),
179
+
),
180
180
)
181
181
.limit(1)
182
182
.then((rows) => rows[0]);
···
195
195
.where(
196
196
and(
197
197
eq(artistAlbums.artistId, artistId),
198
-
eq(artistAlbums.albumId, albumId)
199
-
)
198
+
eq(artistAlbums.albumId, albumId),
199
+
),
200
200
)
201
201
.limit(1)
202
202
.then((rows) => rows[0]);
···
213
213
.select()
214
214
.from(lovedTracks)
215
215
.where(
216
-
and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, trackId))
216
+
and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, trackId)),
217
217
)
218
218
.limit(1)
219
219
.then((rows) => rows[0]);
···
304
304
ctx: Context,
305
305
trackSha256: string,
306
306
user,
307
-
agent: Agent
307
+
agent: Agent,
308
308
) {
309
309
const track = await ctx.db
310
310
.select()
···
321
321
.select()
322
322
.from(lovedTracks)
323
323
.where(
324
-
and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, track.id))
324
+
and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, track.id)),
325
325
)
326
326
.limit(1)
327
327
.then((rows) => rows[0]);
···
351
351
ctx: Context,
352
352
user,
353
353
size = 10,
354
-
offset = 0
354
+
offset = 0,
355
355
) {
356
356
const lovedTracksData = await ctx.db
357
357
.select({
+24
-1
apps/api/src/nowplaying/nowplaying.service.ts
+24
-1
apps/api/src/nowplaying/nowplaying.service.ts
···
10
10
import * as Song from "lexicon/types/app/rocksky/song";
11
11
import { deepSnakeCaseKeys } from "lib";
12
12
import { createHash } from "node:crypto";
13
-
import type { Track } from "types/track";
13
+
import type { MusicbrainzTrack, Track } from "types/track";
14
14
import albumTracks from "../schema/album-tracks";
15
15
import albums from "../schema/albums";
16
16
import artistAlbums from "../schema/artist-albums";
···
22
22
import userArtists from "../schema/user-artists";
23
23
import userTracks from "../schema/user-tracks";
24
24
import users from "../schema/users";
25
+
import tealfm from "../tealfm";
25
26
26
27
export async function putArtistRecord(
27
28
track: Track,
···
822
823
if (existingTrack?.artistUri) {
823
824
console.log(
824
825
`Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`
826
+
);
827
+
}
828
+
829
+
const { data: mbTrack } = await ctx.musicbrainz.post<MusicbrainzTrack>(
830
+
"/hydrate",
831
+
{
832
+
artist: track.artist.split(",").map((a) => ({ name: a.trim() })),
833
+
name: track.title,
834
+
album: track.album,
835
+
}
836
+
);
837
+
838
+
track.mbId = mbTrack?.trackMBID;
839
+
840
+
if (userDid === "did:plc:7vdlgi2bflelz7mmuxoqjfcr" && mbTrack?.trackMBID) {
841
+
mbTrack.timestamp = track.timestamp
842
+
? dayjs.unix(track.timestamp).toISOString()
843
+
: new Date().toISOString();
844
+
await tealfm.publishPlayingNow(
845
+
agent,
846
+
mbTrack,
847
+
Math.floor(track.duration / 1000)
825
848
);
826
849
}
827
850
+1
-3
apps/api/src/schema/album-tracks.ts
+1
-3
apps/api/src/schema/album-tracks.ts
···
4
4
import tracks from "./tracks";
5
5
6
6
const albumTracks = pgTable("album_tracks", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
albumId: text("album_id")
11
9
.notNull()
12
10
.references(() => albums.id),
+1
-3
apps/api/src/schema/albums.ts
+1
-3
apps/api/src/schema/albums.ts
···
2
2
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const albums = pgTable("albums", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
title: text("title").notNull(),
9
7
artist: text("artist").notNull(),
10
8
releaseDate: text("release_date"),
+1
-3
apps/api/src/schema/api-keys.ts
+1
-3
apps/api/src/schema/api-keys.ts
···
3
3
import users from "./users";
4
4
5
5
const apiKeys = pgTable("api_keys", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
name: text("name").notNull(),
10
8
apiKey: text("api_key").notNull(),
11
9
sharedSecret: text("shared_secret").notNull(),
+1
-3
apps/api/src/schema/artist-albums.ts
+1
-3
apps/api/src/schema/artist-albums.ts
···
4
4
import artists from "./artists";
5
5
6
6
const artistAlbums = pgTable("artist_albums", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
artistId: text("artist_id")
11
9
.notNull()
12
10
.references(() => artists.id),
+1
-3
apps/api/src/schema/artist-tracks.ts
+1
-3
apps/api/src/schema/artist-tracks.ts
···
4
4
import tracks from "./tracks";
5
5
6
6
const artistTracks = pgTable("artist_tracks", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
artistId: text("artist_id")
11
9
.notNull()
12
10
.references(() => artists.id),
+1
-3
apps/api/src/schema/artists.ts
+1
-3
apps/api/src/schema/artists.ts
···
2
2
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const artists = pgTable("artists", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
name: text("name").notNull(),
9
7
biography: text("biography"),
10
8
born: timestamp("born"),
+1
-3
apps/api/src/schema/dropbox-accounts.ts
+1
-3
apps/api/src/schema/dropbox-accounts.ts
···
3
3
import users from "./users";
4
4
5
5
const dropboxAccounts = pgTable("dropbox_accounts", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
email: text("email").unique().notNull(),
10
8
isBetaUser: boolean("is_beta_user").default(false).notNull(),
11
9
userId: text("user_id")
+1
-3
apps/api/src/schema/dropbox-directories.ts
+1
-3
apps/api/src/schema/dropbox-directories.ts
···
2
2
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const dropboxDirectories = pgTable("dropbox_directories", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
name: text("name").notNull(),
9
7
path: text("path").notNull(),
10
8
parentId: text("parent_id").references(() => dropboxDirectories.id),
+1
-3
apps/api/src/schema/dropbox-paths.ts
+1
-3
apps/api/src/schema/dropbox-paths.ts
···
3
3
import dropboxDirectories from "./dropbox-directories";
4
4
5
5
const dropboxPaths = pgTable("dropbox_paths", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
path: text("path").notNull(),
10
8
name: text("name").notNull(),
11
9
dropboxId: text("dropbox_id").notNull(),
+1
-3
apps/api/src/schema/dropbox-tokens.ts
+1
-3
apps/api/src/schema/dropbox-tokens.ts
···
2
2
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const dropboxTokens = pgTable("dropbox_tokens", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
refreshToken: text("refresh_token").notNull(),
9
7
createdAt: timestamp("xata_createdat").defaultNow().notNull(),
10
8
updatedAt: timestamp("xata_updatedat").defaultNow().notNull(),
+1
-3
apps/api/src/schema/dropbox.ts
+1
-3
apps/api/src/schema/dropbox.ts
+1
-3
apps/api/src/schema/google-drive-accounts.ts
+1
-3
apps/api/src/schema/google-drive-accounts.ts
···
3
3
import users from "./users";
4
4
5
5
const googleDriveAccounts = pgTable("google_drive_accounts", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
email: text("email").unique().notNull(),
10
8
isBetaUser: boolean("is_beta_user").default(false).notNull(),
11
9
userId: text("user_id")
+1
-3
apps/api/src/schema/google-drive-directories.ts
+1
-3
apps/api/src/schema/google-drive-directories.ts
···
2
2
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const googleDriveDirectories = pgTable("google_drive_directories", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
name: text("name").notNull(),
9
7
path: text("path").notNull(),
10
8
parentId: text("parent_id").references(() => googleDriveDirectories.id),
+1
-3
apps/api/src/schema/google-drive-paths.ts
+1
-3
apps/api/src/schema/google-drive-paths.ts
···
3
3
import googleDriveDirectories from "./google-drive-directories";
4
4
5
5
const googleDrivePaths = pgTable("google_drive_paths", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
googleDriveId: text("google_drive_id").notNull(),
10
8
trackId: text("track_id").notNull(),
11
9
name: text("name").notNull(),
+1
-3
apps/api/src/schema/google-drive-tokens.ts
+1
-3
apps/api/src/schema/google-drive-tokens.ts
···
2
2
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const googleDriveTokens = pgTable("google_drive_tokens", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
refreshToken: text("refresh_token").notNull(),
9
7
createdAt: timestamp("xata_createdat").defaultNow().notNull(),
10
8
updatedAt: timestamp("xata_updatedat").defaultNow().notNull(),
+1
-3
apps/api/src/schema/googledrive.ts
+1
-3
apps/api/src/schema/googledrive.ts
···
4
4
import users from "./users";
5
5
6
6
const googleDrive = pgTable("google_drive", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
googleDriveTokenId: text("google_drive_token_id")
11
9
.notNull()
12
10
.references(() => googleDriveTokens.id),
+1
-3
apps/api/src/schema/loved-tracks.ts
+1
-3
apps/api/src/schema/loved-tracks.ts
···
4
4
import users from "./users";
5
5
6
6
const lovedTracks = pgTable("loved_tracks", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/playlist-tracks.ts
+1
-3
apps/api/src/schema/playlist-tracks.ts
···
4
4
import tracks from "./tracks";
5
5
6
6
const playlistTracks = pgTable("playlist_tracks", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
playlistId: text("playlist_id")
11
9
.notNull()
12
10
.references(() => playlists.id),
+1
-3
apps/api/src/schema/playlists.ts
+1
-3
apps/api/src/schema/playlists.ts
···
3
3
import users from "./users";
4
4
5
5
const playlists = pgTable("playlists", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
name: text("name").notNull(),
10
8
picture: text("picture"),
11
9
description: text("description"),
+1
-3
apps/api/src/schema/profile-shouts.ts
+1
-3
apps/api/src/schema/profile-shouts.ts
···
4
4
import users from "./users";
5
5
6
6
const profileShouts = pgTable("profile_shouts", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/queue-tracks.ts
+1
-3
apps/api/src/schema/queue-tracks.ts
···
4
4
import users from "./users";
5
5
6
6
const queueTracks = pgTable("queue_tracks", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/scrobbles.ts
+1
-3
apps/api/src/schema/scrobbles.ts
···
6
6
import users from "./users";
7
7
8
8
const scrobbles = pgTable("scrobbles", {
9
-
id: text("xata_id")
10
-
.primaryKey()
11
-
.default(sql`xata_id()`),
9
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
12
10
userId: text("user_id").references(() => users.id),
13
11
trackId: text("track_id").references(() => tracks.id),
14
12
albumId: text("album_id").references(() => albums.id),
+1
-3
apps/api/src/schema/shout-likes.ts
+1
-3
apps/api/src/schema/shout-likes.ts
···
4
4
import users from "./users";
5
5
6
6
const shoutLikes = pgTable("shout_likes", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/shout-reports.ts
+1
-3
apps/api/src/schema/shout-reports.ts
···
4
4
import users from "./users";
5
5
6
6
const shoutReports = pgTable("shout_reports", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/shouts.ts
+1
-3
apps/api/src/schema/shouts.ts
···
6
6
import users from "./users";
7
7
8
8
const shouts = pgTable("shouts", {
9
-
id: text("xata_id")
10
-
.primaryKey()
11
-
.default(sql`xata_id()`),
9
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
12
10
content: text("content").notNull(),
13
11
trackId: text("track_id").references(() => tracks.id),
14
12
artistId: text("artist_id").references(() => users.id),
+1
-3
apps/api/src/schema/spotify-accounts.ts
+1
-3
apps/api/src/schema/spotify-accounts.ts
···
9
9
import users from "./users";
10
10
11
11
const spotifyAccounts = pgTable("spotify_accounts", {
12
-
id: text("xata_id")
13
-
.primaryKey()
14
-
.default(sql`xata_id()`),
12
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
15
13
xataVersion: integer("xata_version"),
16
14
email: text("email").notNull(),
17
15
userId: text("user_id")
+1
-3
apps/api/src/schema/spotify-tokens.ts
+1
-3
apps/api/src/schema/spotify-tokens.ts
···
3
3
import users from "./users";
4
4
5
5
const spotifyTokens = pgTable("spotify_tokens", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
xataVersion: integer("xata_version"),
10
8
accessToken: text("access_token").notNull(),
11
9
refreshToken: text("refresh_token").notNull(),
+1
-3
apps/api/src/schema/tracks.ts
+1
-3
apps/api/src/schema/tracks.ts
···
2
2
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const tracks = pgTable("tracks", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
title: text("title").notNull(),
9
7
artist: text("artist").notNull(),
10
8
albumArtist: text("album_artist").notNull(),
+1
-3
apps/api/src/schema/user-albums.ts
+1
-3
apps/api/src/schema/user-albums.ts
···
4
4
import users from "./users";
5
5
6
6
const userAlbums = pgTable("user_albums", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/user-artists.ts
+1
-3
apps/api/src/schema/user-artists.ts
···
4
4
import users from "./users";
5
5
6
6
const userArtists = pgTable("user_artists", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/user-playlists.ts
+1
-3
apps/api/src/schema/user-playlists.ts
···
4
4
import users from "./users";
5
5
6
6
const userPlaylists = pgTable("user_playlists", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/user-tracks.ts
+1
-3
apps/api/src/schema/user-tracks.ts
···
4
4
import users from "./users";
5
5
6
6
const userTracks = pgTable("user_tracks", {
7
-
id: text("xata_id")
8
-
.primaryKey()
9
-
.default(sql`xata_id()`),
7
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
10
8
userId: text("user_id")
11
9
.notNull()
12
10
.references(() => users.id),
+1
-3
apps/api/src/schema/users.ts
+1
-3
apps/api/src/schema/users.ts
···
2
2
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
3
3
4
4
const users = pgTable("users", {
5
-
id: text("xata_id")
6
-
.primaryKey()
7
-
.default(sql`xata_id()`),
5
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
8
6
did: text("did").unique().notNull(),
9
7
displayName: text("display_name"),
10
8
handle: text("handle").unique().notNull(),
+1
-3
apps/api/src/schema/webscrobblers.ts
+1
-3
apps/api/src/schema/webscrobblers.ts
···
3
3
import users from "./users";
4
4
5
5
const webscrobblers = pgTable("webscrobblers", {
6
-
id: text("xata_id")
7
-
.primaryKey()
8
-
.default(sql`xata_id()`),
6
+
id: text("xata_id").primaryKey().default(sql`xata_id()`),
9
7
name: text("name").notNull(),
10
8
uuid: text("uuid").notNull(),
11
9
description: text("description"),
+3
-3
apps/api/src/scripts/avatar.ts
+3
-3
apps/api/src/scripts/avatar.ts
···
19
19
}
20
20
21
21
const plc = await fetch(`https://plc.directory/${user.did}`).then((res) =>
22
-
res.json()
22
+
res.json(),
23
23
);
24
24
25
25
const serviceEndpoint = _.get(plc, "service.0.serviceEndpoint");
···
29
29
}
30
30
31
31
const profile = await fetch(
32
-
`${serviceEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${user.did}&collection=app.bsky.actor.profile&rkey=self`
32
+
`${serviceEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${user.did}&collection=app.bsky.actor.profile&rkey=self`,
33
33
).then((res) => res.json());
34
34
const ref = _.get(profile, "value.avatar.ref.$link");
35
35
const type = _.get(profile, "value.avatar.mimeType", "").split("/")[1];
···
53
53
54
54
ctx.nc.publish(
55
55
"rocksky.user",
56
-
Buffer.from(JSON.stringify(deepSnakeCaseKeys(u)))
56
+
Buffer.from(JSON.stringify(deepSnakeCaseKeys(u))),
57
57
);
58
58
}
59
59
+3
-3
apps/api/src/scripts/genres.ts
+3
-3
apps/api/src/scripts/genres.ts
···
11
11
.from(tables.spotifyTokens)
12
12
.leftJoin(
13
13
tables.spotifyAccounts,
14
-
eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId)
14
+
eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId),
15
15
)
16
16
.where(eq(tables.spotifyAccounts.isBetaUser, true))
17
17
.execute()
···
51
51
headers: {
52
52
Authorization: `Bearer ${token}`,
53
53
},
54
-
}
54
+
},
55
55
)
56
56
.then(
57
57
(res) =>
···
64
64
images: Array<{ url: string }>;
65
65
}>;
66
66
};
67
-
}>
67
+
}>,
68
68
)
69
69
.then(async (data) => _.get(data, "artists.items.0"));
70
70
+4
-4
apps/api/src/scripts/meili.ts
+4
-4
apps/api/src/scripts/meili.ts
···
32
32
for (let i = 0; i < total; i += size) {
33
33
const skip = i;
34
34
console.log(
35
-
`Processing ${chalk.magentaBright("albums")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`
35
+
`Processing ${chalk.magentaBright("albums")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
36
36
);
37
37
const results = await ctx.db
38
38
.select()
···
56
56
for (let i = 0; i < total; i += size) {
57
57
const skip = i;
58
58
console.log(
59
-
`Processing ${chalk.magentaBright("artists")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`
59
+
`Processing ${chalk.magentaBright("artists")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
60
60
);
61
61
const results = await ctx.db
62
62
.select()
···
80
80
for (let i = 0; i < total; i += size) {
81
81
const skip = i;
82
82
console.log(
83
-
`Processing ${chalk.magentaBright("tracks")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`
83
+
`Processing ${chalk.magentaBright("tracks")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
84
84
);
85
85
const results = await ctx.db
86
86
.select()
···
105
105
for (let i = 0; i < total; i += size) {
106
106
const skip = i;
107
107
console.log(
108
-
`Processing ${chalk.magentaBright("users")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`
108
+
`Processing ${chalk.magentaBright("users")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
109
109
);
110
110
const results = await ctx.db
111
111
.select()
+7
-7
apps/api/src/shouts/shouts.service.ts
+7
-7
apps/api/src/shouts/shouts.service.ts
···
21
21
shout: Shout,
22
22
uri: string,
23
23
user,
24
-
agent: Agent
24
+
agent: Agent,
25
25
) {
26
26
let album: SelectAlbum,
27
27
artist: SelectArtist,
···
157
157
reply: Shout,
158
158
shoutUri: string,
159
159
user,
160
-
agent: Agent
160
+
agent: Agent,
161
161
) {
162
162
const shout = await ctx.db
163
163
.select({
···
212
212
profileRecord.uri;
213
213
214
214
let service = await fetch(
215
-
`https://plc.directory/${subjectUri.split("/").slice(0, 3).join("/").split("at://")[1]}`
215
+
`https://plc.directory/${subjectUri.split("/").slice(0, 3).join("/").split("at://")[1]}`,
216
216
)
217
217
.then((res) => res.json<{ service: { seviceEndpoint: string }[] }>())
218
218
.then((data) => data.service);
···
236
236
}
237
237
238
238
service = await fetch(
239
-
`https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`
239
+
`https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`,
240
240
)
241
241
.then((res) => res.json<{ service: { seviceEndpoint: string }[] }>())
242
242
.then((data) => data.service);
···
322
322
ctx: Context,
323
323
shoutUri: string,
324
324
user,
325
-
agent: Agent
325
+
agent: Agent,
326
326
) {
327
327
const rkey = TID.nextStr();
328
328
···
342
342
}
343
343
344
344
const { service } = await fetch(
345
-
`https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`
345
+
`https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`,
346
346
).then((res) => res.json<{ service: [{ serviceEndpoint: string }] }>());
347
347
348
348
const atpAgent = new AtpAgent({
···
410
410
ctx: Context,
411
411
shoutUri: string,
412
412
user,
413
-
agent: Agent
413
+
agent: Agent,
414
414
) {
415
415
const likes = await ctx.db
416
416
.select({
+13
-13
apps/api/src/spotify/app.ts
+13
-13
apps/api/src/spotify/app.ts
···
22
22
limit: 10, // max Spotify API calls
23
23
window: 15, // per 10 seconds
24
24
keyPrefix: "spotify-ratelimit",
25
-
})
25
+
}),
26
26
);
27
27
28
28
app.get("/login", async (c) => {
···
55
55
const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${env.SPOTIFY_CLIENT_ID}&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}`;
56
56
c.header(
57
57
"Set-Cookie",
58
-
`session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`
58
+
`session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`,
59
59
);
60
60
return c.json({ redirectUrl });
61
61
});
···
133
133
.where(
134
134
and(
135
135
eq(spotifyAccounts.userId, user.id),
136
-
eq(spotifyAccounts.isBetaUser, true)
137
-
)
136
+
eq(spotifyAccounts.isBetaUser, true),
137
+
),
138
138
)
139
139
.limit(1)
140
140
.then((rows) => rows[0]);
···
251
251
}
252
252
253
253
const cached = await ctx.redis.get(
254
-
`${spotifyAccount.spotifyAccount.email}:current`
254
+
`${spotifyAccount.spotifyAccount.email}:current`,
255
255
);
256
256
if (!cached) {
257
257
return c.json({});
···
261
261
262
262
const sha256 = createHash("sha256")
263
263
.update(
264
-
`${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase()
264
+
`${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase(),
265
265
)
266
266
.digest("hex");
267
267
···
334
334
335
335
const refreshToken = decrypt(
336
336
spotifyToken.refreshToken,
337
-
env.SPOTIFY_ENCRYPTION_KEY
337
+
env.SPOTIFY_ENCRYPTION_KEY,
338
338
);
339
339
340
340
// get new access token
···
410
410
411
411
const refreshToken = decrypt(
412
412
spotifyToken.refreshToken,
413
-
env.SPOTIFY_ENCRYPTION_KEY
413
+
env.SPOTIFY_ENCRYPTION_KEY,
414
414
);
415
415
416
416
// get new access token
···
486
486
487
487
const refreshToken = decrypt(
488
488
spotifyToken.refreshToken,
489
-
env.SPOTIFY_ENCRYPTION_KEY
489
+
env.SPOTIFY_ENCRYPTION_KEY,
490
490
);
491
491
492
492
// get new access token
···
562
562
563
563
const refreshToken = decrypt(
564
564
spotifyToken.refreshToken,
565
-
env.SPOTIFY_ENCRYPTION_KEY
565
+
env.SPOTIFY_ENCRYPTION_KEY,
566
566
);
567
567
568
568
// get new access token
···
590
590
headers: {
591
591
Authorization: `Bearer ${access_token}`,
592
592
},
593
-
}
593
+
},
594
594
);
595
595
596
596
if (response.status === 403) {
···
641
641
642
642
const refreshToken = decrypt(
643
643
spotifyToken.refreshToken,
644
-
env.SPOTIFY_ENCRYPTION_KEY
644
+
env.SPOTIFY_ENCRYPTION_KEY,
645
645
);
646
646
647
647
// get new access token
···
670
670
headers: {
671
671
Authorization: `Bearer ${access_token}`,
672
672
},
673
-
}
673
+
},
674
674
);
675
675
676
676
if (response.status === 403) {
+51
apps/api/src/tealfm/index.ts
+51
apps/api/src/tealfm/index.ts
···
1
+
import type { Agent } from "@atproto/api";
2
+
import { TID } from "@atproto/common";
3
+
import chalk from "chalk";
4
+
import * as Play from "lexicon/types/fm/teal/alpha/feed/play";
5
+
import type { MusicbrainzTrack } from "types/track";
6
+
7
+
const SUBMISSION_CLIENT_AGENT = "rocksky/v0.0.1";
8
+
9
+
async function publishPlayingNow(
10
+
agent: Agent,
11
+
track: MusicbrainzTrack,
12
+
duration: number
13
+
) {
14
+
try {
15
+
const rkey = TID.nextStr();
16
+
const record: Play.Record = {
17
+
$type: "fm.teal.alpha.feed.play",
18
+
duration,
19
+
trackName: track.name,
20
+
playedTime: track.timestamp,
21
+
artists: track.artist.map((artist) => ({
22
+
artistMbid: artist.mbid,
23
+
artistName: artist.name,
24
+
})),
25
+
releaseMbid: track.releaseMBID,
26
+
releaseName: track.album,
27
+
recordingMbId: track.trackMBID,
28
+
submissionClientAgent: SUBMISSION_CLIENT_AGENT,
29
+
};
30
+
31
+
if (!Play.validateRecord(record).success) {
32
+
console.log(Play.validateRecord(record));
33
+
console.log(chalk.cyan(JSON.stringify(record, null, 2)));
34
+
throw new Error("Invalid record");
35
+
}
36
+
37
+
const res = await agent.com.atproto.repo.putRecord({
38
+
repo: agent.assertDid,
39
+
collection: "fm.teal.alpha.feed.play",
40
+
rkey,
41
+
record,
42
+
validate: false,
43
+
});
44
+
const uri = res.data.uri;
45
+
console.log(`tealfm Play record created at ${uri}`);
46
+
} catch (error) {
47
+
console.error("Error publishing teal.fm record:", error);
48
+
}
49
+
}
50
+
51
+
export default { publishPlayingNow };
+60
apps/api/src/tealfm/lexicons/teal/actor/defs.json
+60
apps/api/src/tealfm/lexicons/teal/actor/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.actor.defs",
4
+
"defs": {
5
+
"profileView": {
6
+
"type": "object",
7
+
"properties": {
8
+
"did": {
9
+
"type": "string",
10
+
"description": "The decentralized identifier of the actor"
11
+
},
12
+
"displayName": {
13
+
"type": "string"
14
+
},
15
+
"description": {
16
+
"type": "string",
17
+
"description": "Free-form profile description text."
18
+
},
19
+
"descriptionFacets": {
20
+
"type": "array",
21
+
"description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon.",
22
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
23
+
},
24
+
"featuredItem": {
25
+
"type": "ref",
26
+
"description": "The user's most recent item featured on their profile.",
27
+
"ref": "fm.teal.alpha.actor.profile#featuredItem"
28
+
},
29
+
"avatar": {
30
+
"type": "string",
31
+
"description": "IPLD of the avatar"
32
+
},
33
+
"banner": {
34
+
"type": "string",
35
+
"description": "IPLD of the banner image"
36
+
},
37
+
"createdAt": { "type": "string", "format": "datetime" }
38
+
}
39
+
},
40
+
"miniProfileView": {
41
+
"type": "object",
42
+
"properties": {
43
+
"did": {
44
+
"type": "string",
45
+
"description": "The decentralized identifier of the actor"
46
+
},
47
+
"displayName": {
48
+
"type": "string"
49
+
},
50
+
"handle": {
51
+
"type": "string"
52
+
},
53
+
"avatar": {
54
+
"type": "string",
55
+
"description": "IPLD of the avatar"
56
+
}
57
+
}
58
+
}
59
+
}
60
+
}
+34
apps/api/src/tealfm/lexicons/teal/actor/getProfile.json
+34
apps/api/src/tealfm/lexicons/teal/actor/getProfile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.actor.getProfile",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "query",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actor"],
11
+
"properties": {
12
+
"actor": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The author's DID"
16
+
}
17
+
}
18
+
},
19
+
"output": {
20
+
"encoding": "application/json",
21
+
"schema": {
22
+
"type": "object",
23
+
"required": ["actor"],
24
+
"properties": {
25
+
"actor": {
26
+
"type": "ref",
27
+
"ref": "fm.teal.alpha.actor.defs#profileView"
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
34
+
}
+40
apps/api/src/tealfm/lexicons/teal/actor/getProfiles.json
+40
apps/api/src/tealfm/lexicons/teal/actor/getProfiles.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.actor.getProfiles",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves the associated profile.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "query",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["actors"],
11
+
"properties": {
12
+
"actors": {
13
+
"type": "array",
14
+
"items": {
15
+
"type": "string",
16
+
"format": "at-identifier"
17
+
},
18
+
"description": "Array of actor DIDs"
19
+
}
20
+
}
21
+
},
22
+
"output": {
23
+
"encoding": "application/json",
24
+
"schema": {
25
+
"type": "object",
26
+
"required": ["actors"],
27
+
"properties": {
28
+
"actors": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "ref",
32
+
"ref": "fm.teal.alpha.actor.defs#miniProfileView"
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
+64
apps/api/src/tealfm/lexicons/teal/actor/profile.json
+64
apps/api/src/tealfm/lexicons/teal/actor/profile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.actor.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm account profile.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"properties": {
12
+
"displayName": {
13
+
"type": "string",
14
+
"maxGraphemes": 64,
15
+
"maxLength": 640
16
+
},
17
+
"description": {
18
+
"type": "string",
19
+
"description": "Free-form profile description text.",
20
+
"maxGraphemes": 256,
21
+
"maxLength": 2560
22
+
},
23
+
"descriptionFacets": {
24
+
"type": "array",
25
+
"description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc).",
26
+
"items": { "type": "ref", "ref": "app.bsky.richtext.facet" }
27
+
},
28
+
"featuredItem": {
29
+
"type": "ref",
30
+
"description": "The user's most recent item featured on their profile.",
31
+
"ref": "#featuredItem"
32
+
},
33
+
"avatar": {
34
+
"type": "blob",
35
+
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
36
+
"accept": ["image/png", "image/jpeg"],
37
+
"maxSize": 1000000
38
+
},
39
+
"banner": {
40
+
"type": "blob",
41
+
"description": "Larger horizontal image to display behind profile view.",
42
+
"accept": ["image/png", "image/jpeg"],
43
+
"maxSize": 1000000
44
+
},
45
+
"createdAt": { "type": "string", "format": "datetime" }
46
+
}
47
+
}
48
+
},
49
+
"featuredItem": {
50
+
"type": "object",
51
+
"required": ["mbid", "type"],
52
+
"properties": {
53
+
"mbid": {
54
+
"type": "string",
55
+
"description": "The Musicbrainz ID of the item"
56
+
},
57
+
"type": {
58
+
"type": "string",
59
+
"description": "The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc."
60
+
}
61
+
}
62
+
}
63
+
}
64
+
}
+52
apps/api/src/tealfm/lexicons/teal/actor/searchActors.json
+52
apps/api/src/tealfm/lexicons/teal/actor/searchActors.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.actor.searchActors",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | Searches for actors based on profile contents.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "query",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["q"],
11
+
"properties": {
12
+
"q": {
13
+
"type": "string",
14
+
"description": "The search query",
15
+
"maxGraphemes": 128,
16
+
"maxLength": 640
17
+
},
18
+
"limit": {
19
+
"type": "integer",
20
+
"description": "The maximum number of actors to return",
21
+
"minimum": 1,
22
+
"maximum": 25
23
+
},
24
+
"cursor": {
25
+
"type": "string",
26
+
"description": "Cursor for pagination"
27
+
}
28
+
}
29
+
},
30
+
"output": {
31
+
"encoding": "application/json",
32
+
"schema": {
33
+
"type": "object",
34
+
"required": ["actors"],
35
+
"properties": {
36
+
"actors": {
37
+
"type": "array",
38
+
"items": {
39
+
"type": "ref",
40
+
"ref": "fm.teal.alpha.actor.defs#miniProfileView"
41
+
}
42
+
},
43
+
"cursor": {
44
+
"type": "string",
45
+
"description": "Cursor for pagination"
46
+
}
47
+
}
48
+
}
49
+
}
50
+
}
51
+
}
52
+
}
+31
apps/api/src/tealfm/lexicons/teal/actor/status.json
+31
apps/api/src/tealfm/lexicons/teal/actor/status.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.actor.status",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of the status of the actor. Only one can be shown at a time. If there are multiple, the latest record should be picked and earlier records should be deleted or tombstoned.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["time", "item"],
12
+
"properties": {
13
+
"time": {
14
+
"type": "string",
15
+
"format": "datetime",
16
+
"description": "The unix timestamp of when the item was recorded"
17
+
},
18
+
"expiry": {
19
+
"type": "string",
20
+
"format": "datetime",
21
+
"description": "The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time."
22
+
},
23
+
"item": {
24
+
"type": "ref",
25
+
"ref": "fm.teal.alpha.feed.defs#playView"
26
+
}
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
+90
apps/api/src/tealfm/lexicons/teal/feed/defs.json
+90
apps/api/src/tealfm/lexicons/teal/feed/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.feed.defs",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | Misc. items related to feeds.",
5
+
"defs": {
6
+
"playView": {
7
+
"type": "object",
8
+
"required": ["trackName", "artists"],
9
+
"properties": {
10
+
"trackName": {
11
+
"type": "string",
12
+
"minLength": 1,
13
+
"maxLength": 256,
14
+
"maxGraphemes": 2560,
15
+
"description": "The name of the track"
16
+
},
17
+
"trackMbId": {
18
+
"type": "string",
19
+
"description": "The Musicbrainz ID of the track"
20
+
},
21
+
"recordingMbId": {
22
+
"type": "string",
23
+
"description": "The Musicbrainz recording ID of the track"
24
+
},
25
+
"duration": {
26
+
"type": "integer",
27
+
"description": "The length of the track in seconds"
28
+
},
29
+
"artists": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "ref",
33
+
"ref": "#artist"
34
+
},
35
+
"description": "Array of artists in order of original appearance."
36
+
},
37
+
"releaseName": {
38
+
"type": "string",
39
+
"maxLength": 256,
40
+
"maxGraphemes": 2560,
41
+
"description": "The name of the release/album"
42
+
},
43
+
"releaseMbId": {
44
+
"type": "string",
45
+
"description": "The Musicbrainz release ID"
46
+
},
47
+
"isrc": {
48
+
"type": "string",
49
+
"description": "The ISRC code associated with the recording"
50
+
},
51
+
"originUrl": {
52
+
"type": "string",
53
+
"description": "The URL associated with this track"
54
+
},
55
+
"musicServiceBaseDomain": {
56
+
"type": "string",
57
+
"description": "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided."
58
+
},
59
+
"submissionClientAgent": {
60
+
"type": "string",
61
+
"maxLength": 256,
62
+
"maxGraphemes": 2560,
63
+
"description": "A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided."
64
+
},
65
+
"playedTime": {
66
+
"type": "string",
67
+
"format": "datetime",
68
+
"description": "The unix timestamp of when the track was played"
69
+
}
70
+
}
71
+
},
72
+
"artist": {
73
+
"type": "object",
74
+
"required": ["artistName"],
75
+
"properties": {
76
+
"artistName": {
77
+
"type": "string",
78
+
"minLength": 1,
79
+
"maxLength": 256,
80
+
"maxGraphemes": 2560,
81
+
"description": "The name of the artist"
82
+
},
83
+
"artistMbId": {
84
+
"type": "string",
85
+
"description": "The Musicbrainz ID of the artist"
86
+
}
87
+
}
88
+
}
89
+
}
90
+
}
+45
apps/api/src/tealfm/lexicons/teal/feed/getActorFeed.json
+45
apps/api/src/tealfm/lexicons/teal/feed/getActorFeed.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.feed.getActorFeed",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves multiple plays from the index or via an author's DID.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "query",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["authorDID"],
11
+
"properties": {
12
+
"authorDID": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The author's DID for the play"
16
+
},
17
+
"cursor": {
18
+
"type": "string",
19
+
"description": "The cursor to start the query from"
20
+
},
21
+
"limit": {
22
+
"type": "integer",
23
+
"description": "The upper limit of tracks to get per request. Default is 20, max is 50."
24
+
}
25
+
}
26
+
},
27
+
"output": {
28
+
"encoding": "application/json",
29
+
"schema": {
30
+
"type": "object",
31
+
"required": ["plays"],
32
+
"properties": {
33
+
"plays": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "ref",
37
+
"ref": "fm.teal.alpha.feed.defs#playView"
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
44
+
}
45
+
}
+38
apps/api/src/tealfm/lexicons/teal/feed/getPlay.json
+38
apps/api/src/tealfm/lexicons/teal/feed/getPlay.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.feed.getPlay",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "query",
8
+
"parameters": {
9
+
"type": "params",
10
+
"required": ["authorDID", "rkey"],
11
+
"properties": {
12
+
"authorDID": {
13
+
"type": "string",
14
+
"format": "at-identifier",
15
+
"description": "The author's DID for the play"
16
+
},
17
+
"rkey": {
18
+
"type": "string",
19
+
"description": "The record key of the play"
20
+
}
21
+
}
22
+
},
23
+
"output": {
24
+
"encoding": "application/json",
25
+
"schema": {
26
+
"type": "object",
27
+
"required": ["play"],
28
+
"properties": {
29
+
"play": {
30
+
"type": "ref",
31
+
"ref": "fm.teal.alpha.feed.defs#playView"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
38
+
}
+95
apps/api/src/tealfm/lexicons/teal/feed/play.json
+95
apps/api/src/tealfm/lexicons/teal/feed/play.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "fm.teal.alpha.feed.play",
4
+
"description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm play. Plays are submitted as a result of a user listening to a track. Plays should be marked as tracked when a user has listened to the entire track if it's under 2 minutes long, or half of the track's duration up to 4 minutes, whichever is longest.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "record",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["trackName"],
12
+
"properties": {
13
+
"trackName": {
14
+
"type": "string",
15
+
"minLength": 1,
16
+
"maxLength": 256,
17
+
"maxGraphemes": 2560,
18
+
"description": "The name of the track"
19
+
},
20
+
"trackMbId": {
21
+
"type": "string",
22
+
23
+
"description": "The Musicbrainz ID of the track"
24
+
},
25
+
"recordingMbId": {
26
+
"type": "string",
27
+
"description": "The Musicbrainz recording ID of the track"
28
+
},
29
+
"duration": {
30
+
"type": "integer",
31
+
"description": "The length of the track in seconds"
32
+
},
33
+
"artistNames": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "string",
37
+
"minLength": 1,
38
+
"maxLength": 256,
39
+
"maxGraphemes": 2560
40
+
},
41
+
"description": "Array of artist names in order of original appearance. Prefer using 'artists'."
42
+
},
43
+
"artistMbIds": {
44
+
"type": "array",
45
+
"items": {
46
+
"type": "string"
47
+
},
48
+
"description": "Array of Musicbrainz artist IDs. Prefer using 'artists'."
49
+
},
50
+
"artists": {
51
+
"type": "array",
52
+
"items": {
53
+
"type": "ref",
54
+
"ref": "fm.teal.alpha.feed.defs#artist"
55
+
},
56
+
"description": "Array of artists in order of original appearance."
57
+
},
58
+
"releaseName": {
59
+
"type": "string",
60
+
"maxLength": 256,
61
+
"maxGraphemes": 2560,
62
+
"description": "The name of the release/album"
63
+
},
64
+
"releaseMbId": {
65
+
"type": "string",
66
+
"description": "The Musicbrainz release ID"
67
+
},
68
+
"isrc": {
69
+
"type": "string",
70
+
"description": "The ISRC code associated with the recording"
71
+
},
72
+
"originUrl": {
73
+
"type": "string",
74
+
"description": "The URL associated with this track"
75
+
},
76
+
"musicServiceBaseDomain": {
77
+
"type": "string",
78
+
"description": "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided."
79
+
},
80
+
"submissionClientAgent": {
81
+
"type": "string",
82
+
"maxLength": 256,
83
+
"maxGraphemes": 2560,
84
+
"description": "A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided."
85
+
},
86
+
"playedTime": {
87
+
"type": "string",
88
+
"format": "datetime",
89
+
"description": "The unix timestamp of when the track was played"
90
+
}
91
+
}
92
+
}
93
+
}
94
+
}
95
+
}
+9
-9
apps/api/src/tracks/tracks.service.ts
+9
-9
apps/api/src/tracks/tracks.service.ts
···
132
132
if (!track_id || !album_id || !artist_id) {
133
133
console.log(
134
134
"Track not yet saved (uri not saved), retrying...",
135
-
tries + 1
135
+
tries + 1,
136
136
);
137
137
await new Promise((resolve) => setTimeout(resolve, 1000));
138
138
tries += 1;
···
145
145
.where(
146
146
and(
147
147
eq(albumTracks.albumId, album_id.id),
148
-
eq(albumTracks.trackId, track_id.id)
149
-
)
148
+
eq(albumTracks.trackId, track_id.id),
149
+
),
150
150
)
151
151
.limit(1)
152
152
.then((results) => results[0]);
···
157
157
.where(
158
158
and(
159
159
eq(artistTracks.artistId, artist_id.id),
160
-
eq(artistTracks.trackId, track_id.id)
161
-
)
160
+
eq(artistTracks.trackId, track_id.id),
161
+
),
162
162
)
163
163
.limit(1)
164
164
.then((results) => results[0]);
···
169
169
.where(
170
170
and(
171
171
eq(artistAlbums.artistId, artist_id.id),
172
-
eq(artistAlbums.albumId, album_id.id)
173
-
)
172
+
eq(artistAlbums.albumId, album_id.id),
173
+
),
174
174
)
175
175
.limit(1)
176
176
.then((results) => results[0]);
···
264
264
xata_createdat: artist_album.createdAt.toISOString(),
265
265
xata_updatedat: artist_album.updatedAt.toISOString(),
266
266
},
267
-
})
267
+
}),
268
268
);
269
269
270
270
ctx.nc.publish(
271
271
"rocksky.track",
272
-
Buffer.from(message.replaceAll("sha_256", "sha256"))
272
+
Buffer.from(message.replaceAll("sha_256", "sha256")),
273
273
);
274
274
break;
275
275
}
+13
apps/api/src/types/track.ts
+13
apps/api/src/types/track.ts
···
34
34
});
35
35
36
36
export type Track = z.infer<typeof trackSchema>;
37
+
38
+
export type MusicbrainzTrack = {
39
+
trackMBID: string;
40
+
releaseMBID: string;
41
+
name: string;
42
+
artist: {
43
+
id: string;
44
+
mbid: string;
45
+
name: string;
46
+
}[];
47
+
album: string;
48
+
timestamp: string;
49
+
};
+32
-32
apps/api/src/users/app.ts
+32
-32
apps/api/src/users/app.ts
···
20
20
import { requestCounter } from "metrics";
21
21
import * as R from "ramda";
22
22
import tables from "schema";
23
-
import { SelectUser } from "schema/users";
23
+
import type { SelectUser } from "schema/users";
24
24
import {
25
25
createShout,
26
26
likeShout,
···
123
123
data.map((item) => ({
124
124
...item,
125
125
tags: [],
126
-
}))
126
+
})),
127
127
);
128
128
});
129
129
···
153
153
results.map((x) => ({
154
154
...x.playlists,
155
155
trackCount: +x.trackCount,
156
-
}))
156
+
})),
157
157
);
158
158
});
159
159
···
219
219
.from(tables.userArtists)
220
220
.leftJoin(
221
221
tables.artists,
222
-
eq(tables.userArtists.artistId, tables.artists.id)
222
+
eq(tables.userArtists.artistId, tables.artists.id),
223
223
)
224
224
.where(or(eq(tables.userArtists.uri, uri), eq(tables.artists.uri, uri)))
225
225
.limit(1)
···
309
309
scrobbles: scrobbles || 1,
310
310
label: tracks[0]?.tracks.label || "",
311
311
tracks: dedupeTracksKeepLyrics(tracks.map((track) => track.tracks)).sort(
312
-
(a, b) => a.track_number - b.track_number
312
+
(a, b) => a.track_number - b.track_number,
313
313
),
314
314
tags: [],
315
315
});
···
398
398
...R.omit(["id"], item.tracks),
399
399
id: item.tracks.id,
400
400
xata_version: item.artist_tracks.xataVersion,
401
-
}))
401
+
})),
402
402
);
403
403
});
404
404
···
430
430
...R.omit(["id"], item.albums),
431
431
id: item.albums.id,
432
432
xata_version: item.artist_albums.xataVersion,
433
-
}))
434
-
)
433
+
})),
434
+
),
435
435
);
436
436
});
437
437
···
461
461
.from(tables.playlistTracks)
462
462
.leftJoin(
463
463
tables.playlists,
464
-
eq(tables.playlistTracks.playlistId, tables.playlists.id)
464
+
eq(tables.playlistTracks.playlistId, tables.playlists.id),
465
465
)
466
466
.leftJoin(
467
467
tables.tracks,
468
-
eq(tables.playlistTracks.trackId, tables.tracks.id)
468
+
eq(tables.playlistTracks.trackId, tables.tracks.id),
469
469
)
470
470
.where(eq(tables.playlists.uri, uri))
471
471
.groupBy(
···
509
509
tables.playlists.picture,
510
510
tables.playlists.spotifyLink,
511
511
tables.playlists.tidalLink,
512
-
tables.playlists.appleMusicLink
512
+
tables.playlists.appleMusicLink,
513
513
)
514
514
.orderBy(asc(tables.playlistTracks.createdAt))
515
515
.execute();
···
632
632
parsed.data,
633
633
`at://${did}/app.rocksky.artist/${rkey}`,
634
634
user,
635
-
agent
635
+
agent,
636
636
);
637
637
return c.json({});
638
638
});
···
681
681
parsed.data,
682
682
`at://${did}/app.rocksky.album/${rkey}`,
683
683
user,
684
-
agent
684
+
agent,
685
685
);
686
686
return c.json({});
687
687
});
···
730
730
parsed.data,
731
731
`at://${did}/app.rocksky.song/${rkey}`,
732
732
user,
733
-
agent
733
+
agent,
734
734
);
735
735
736
736
return c.json({});
···
780
780
parsed.data,
781
781
`at://${did}/app.rocksky.scrobble/${rkey}`,
782
782
user,
783
-
agent
783
+
agent,
784
784
);
785
785
786
786
return c.json({});
···
1063
1063
parsed.data,
1064
1064
`at://${did}/app.rocksky.shout/${rkey}`,
1065
1065
user,
1066
-
agent
1066
+
agent,
1067
1067
);
1068
1068
return c.json({});
1069
1069
});
···
1132
1132
.leftJoin(tables.artists, eq(tables.shouts.artistId, tables.artists.id))
1133
1133
.leftJoin(
1134
1134
tables.shoutLikes,
1135
-
eq(tables.shouts.id, tables.shoutLikes.shoutId)
1135
+
eq(tables.shouts.id, tables.shoutLikes.shoutId),
1136
1136
)
1137
1137
.where(eq(tables.artists.uri, `at://${did}/app.rocksky.artist/${rkey}`))
1138
1138
.groupBy(
···
1145
1145
tables.users.did,
1146
1146
tables.users.handle,
1147
1147
tables.users.displayName,
1148
-
tables.users.avatar
1148
+
tables.users.avatar,
1149
1149
)
1150
1150
.orderBy(desc(tables.shouts.createdAt))
1151
1151
.execute();
···
1217
1217
.leftJoin(tables.albums, eq(tables.shouts.albumId, tables.albums.id))
1218
1218
.leftJoin(
1219
1219
tables.shoutLikes,
1220
-
eq(tables.shouts.id, tables.shoutLikes.shoutId)
1220
+
eq(tables.shouts.id, tables.shoutLikes.shoutId),
1221
1221
)
1222
1222
.where(eq(tables.albums.uri, `at://${did}/app.rocksky.album/${rkey}`))
1223
1223
.groupBy(
···
1230
1230
tables.users.did,
1231
1231
tables.users.handle,
1232
1232
tables.users.displayName,
1233
-
tables.users.avatar
1233
+
tables.users.avatar,
1234
1234
)
1235
1235
.orderBy(desc(tables.shouts.createdAt))
1236
1236
.execute();
···
1302
1302
.leftJoin(tables.tracks, eq(tables.shouts.trackId, tables.tracks.id))
1303
1303
.leftJoin(
1304
1304
tables.shoutLikes,
1305
-
eq(tables.shouts.id, tables.shoutLikes.shoutId)
1305
+
eq(tables.shouts.id, tables.shoutLikes.shoutId),
1306
1306
)
1307
1307
.where(eq(tables.tracks.uri, `at://${did}/app.rocksky.song/${rkey}`))
1308
1308
.groupBy(
···
1315
1315
tables.users.did,
1316
1316
tables.users.handle,
1317
1317
tables.users.displayName,
1318
-
tables.users.avatar
1318
+
tables.users.avatar,
1319
1319
)
1320
1320
.orderBy(desc(tables.shouts.createdAt))
1321
1321
.execute();
···
1386
1386
.leftJoin(tables.users, eq(tables.shouts.authorId, tables.users.id))
1387
1387
.leftJoin(
1388
1388
tables.scrobbles,
1389
-
eq(tables.shouts.scrobbleId, tables.scrobbles.id)
1389
+
eq(tables.shouts.scrobbleId, tables.scrobbles.id),
1390
1390
)
1391
1391
.leftJoin(
1392
1392
tables.shoutLikes,
1393
-
eq(tables.shouts.id, tables.shoutLikes.shoutId)
1393
+
eq(tables.shouts.id, tables.shoutLikes.shoutId),
1394
1394
)
1395
1395
.where(eq(tables.scrobbles.uri, `at://${did}/app.rocksky.scrobble/${rkey}`))
1396
1396
.groupBy(
···
1403
1403
tables.users.did,
1404
1404
tables.users.handle,
1405
1405
tables.users.displayName,
1406
-
tables.users.avatar
1406
+
tables.users.avatar,
1407
1407
)
1408
1408
.orderBy(desc(tables.shouts.createdAt))
1409
1409
.execute();
···
1482
1482
.leftJoin(tables.shouts, eq(tables.profileShouts.shoutId, tables.shouts.id))
1483
1483
.leftJoin(
1484
1484
aliasedTable(tables.users, "authors"),
1485
-
eq(tables.shouts.authorId, aliasedTable(tables.users, "authors").id)
1485
+
eq(tables.shouts.authorId, aliasedTable(tables.users, "authors").id),
1486
1486
)
1487
1487
.leftJoin(tables.users, eq(tables.profileShouts.userId, tables.users.id))
1488
1488
.leftJoin(
1489
1489
tables.shoutLikes,
1490
-
eq(tables.shouts.id, tables.shoutLikes.shoutId)
1490
+
eq(tables.shouts.id, tables.shoutLikes.shoutId),
1491
1491
)
1492
1492
.groupBy(
1493
1493
tables.profileShouts.id,
···
1506
1506
aliasedTable(tables.users, "authors").did,
1507
1507
aliasedTable(tables.users, "authors").handle,
1508
1508
aliasedTable(tables.users, "authors").displayName,
1509
-
aliasedTable(tables.users, "authors").avatar
1509
+
aliasedTable(tables.users, "authors").avatar,
1510
1510
)
1511
1511
.orderBy(desc(tables.profileShouts.createdAt))
1512
1512
.execute();
···
1615
1615
.where(
1616
1616
and(
1617
1617
eq(tables.shoutReports.userId, user.id),
1618
-
eq(tables.shoutReports.shoutId, shout.id)
1619
-
)
1618
+
eq(tables.shoutReports.shoutId, shout.id),
1619
+
),
1620
1620
)
1621
1621
.limit(1)
1622
1622
.execute()
···
1689
1689
.where(
1690
1690
and(
1691
1691
eq(tables.shoutReports.userId, user.id),
1692
-
eq(tables.shoutReports.shoutId, shout.id)
1693
-
)
1692
+
eq(tables.shoutReports.shoutId, shout.id),
1693
+
),
1694
1694
)
1695
1695
.limit(1)
1696
1696
.execute()
+9
-9
apps/api/src/xrpc/app/rocksky/actor/getProfile.ts
+9
-9
apps/api/src/xrpc/app/rocksky/actor/getProfile.ts
···
33
33
Effect.catchAll((err) => {
34
34
console.error(err);
35
35
return Effect.succeed({});
36
-
})
36
+
}),
37
37
);
38
38
server.app.rocksky.actor.getProfile({
39
39
auth: ctx.authVerifier,
···
194
194
.from(tables.spotifyAccounts)
195
195
.leftJoin(
196
196
tables.users,
197
-
eq(tables.spotifyAccounts.userId, tables.users.id)
197
+
eq(tables.spotifyAccounts.userId, tables.users.id),
198
198
)
199
199
.where(eq(tables.users.did, did))
200
200
.execute()
···
204
204
.from(tables.spotifyTokens)
205
205
.leftJoin(
206
206
tables.users,
207
-
eq(tables.spotifyTokens.userId, tables.users.id)
207
+
eq(tables.spotifyTokens.userId, tables.users.id),
208
208
)
209
209
.where(eq(tables.users.did, did))
210
210
.execute()
···
214
214
.from(tables.googleDriveAccounts)
215
215
.leftJoin(
216
216
tables.users,
217
-
eq(tables.googleDriveAccounts.userId, tables.users.id)
217
+
eq(tables.googleDriveAccounts.userId, tables.users.id),
218
218
)
219
219
.where(eq(tables.users.did, did))
220
220
.execute()
···
224
224
.from(tables.dropboxAccounts)
225
225
.leftJoin(
226
226
tables.users,
227
-
eq(tables.dropboxAccounts.userId, tables.users.id)
227
+
eq(tables.dropboxAccounts.userId, tables.users.id),
228
228
)
229
229
.where(eq(tables.users.did, did))
230
230
.execute()
···
281
281
xata_createdat: profile.user.createdAt.toISOString(),
282
282
xata_updatedat: profile.user.updatedAt.toISOString(),
283
283
xata_version: 1,
284
-
})
285
-
)
284
+
}),
285
+
),
286
286
);
287
287
} else {
288
288
// Update existing user in background if handle or avatar or displayName changed
···
315
315
xata_createdat: profile.user.createdAt.toISOString(),
316
316
xata_updatedat: new Date().toISOString(),
317
317
xata_version: (profile.user.xataVersion || 1) + 1,
318
-
})
319
-
)
318
+
}),
319
+
),
320
320
);
321
321
}
322
322
}
+78
musicbrainz/go.mod
+78
musicbrainz/go.mod
···
1
+
module github.com/tsirysndr/rocksky/musicbrainz
2
+
3
+
go 1.25.1
4
+
5
+
require (
6
+
github.com/labstack/echo/v4 v4.13.3
7
+
github.com/teal-fm/piper v0.0.0-20251003185718-473cb159ecbe
8
+
)
9
+
10
+
require (
11
+
github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e // indirect
12
+
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
13
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
14
+
github.com/dlclark/regexp2 v1.11.5 // indirect
15
+
github.com/felixge/httpsnoop v1.0.4 // indirect
16
+
github.com/go-logr/logr v1.4.2 // indirect
17
+
github.com/go-logr/stdr v1.2.2 // indirect
18
+
github.com/goccy/go-json v0.10.2 // indirect
19
+
github.com/gogo/protobuf v1.3.2 // indirect
20
+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
21
+
github.com/google/uuid v1.6.0 // indirect
22
+
github.com/haileyok/atproto-oauth-golang v0.0.2 // indirect
23
+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
24
+
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
25
+
github.com/hashicorp/golang-lru v1.0.2 // indirect
26
+
github.com/ipfs/bbloom v0.0.4 // indirect
27
+
github.com/ipfs/go-block-format v0.2.0 // indirect
28
+
github.com/ipfs/go-cid v0.4.1 // indirect
29
+
github.com/ipfs/go-datastore v0.6.0 // indirect
30
+
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
31
+
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
32
+
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
33
+
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
34
+
github.com/ipfs/go-ipld-format v0.6.0 // indirect
35
+
github.com/ipfs/go-log v1.0.5 // indirect
36
+
github.com/ipfs/go-log/v2 v2.5.1 // indirect
37
+
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
38
+
github.com/jbenet/goprocess v0.1.4 // indirect
39
+
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
40
+
github.com/labstack/gommon v0.4.2 // indirect
41
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
42
+
github.com/lestrrat-go/httpcc v1.0.1 // indirect
43
+
github.com/lestrrat-go/httprc v1.0.4 // indirect
44
+
github.com/lestrrat-go/iter v1.0.2 // indirect
45
+
github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect
46
+
github.com/lestrrat-go/option v1.0.1 // indirect
47
+
github.com/mattn/go-colorable v0.1.13 // indirect
48
+
github.com/mattn/go-isatty v0.0.20 // indirect
49
+
github.com/mattn/go-sqlite3 v1.14.27 // indirect
50
+
github.com/minio/sha256-simd v1.0.1 // indirect
51
+
github.com/mr-tron/base58 v1.2.0 // indirect
52
+
github.com/multiformats/go-base32 v0.1.0 // indirect
53
+
github.com/multiformats/go-base36 v0.2.0 // indirect
54
+
github.com/multiformats/go-multibase v0.2.0 // indirect
55
+
github.com/multiformats/go-multihash v0.2.3 // indirect
56
+
github.com/multiformats/go-varint v0.0.7 // indirect
57
+
github.com/opentracing/opentracing-go v1.2.0 // indirect
58
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
59
+
github.com/segmentio/asm v1.2.0 // indirect
60
+
github.com/spaolacci/murmur3 v1.1.0 // indirect
61
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
62
+
github.com/valyala/fasttemplate v1.2.2 // indirect
63
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
64
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
65
+
go.opentelemetry.io/otel v1.29.0 // indirect
66
+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
67
+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
68
+
go.uber.org/atomic v1.11.0 // indirect
69
+
go.uber.org/multierr v1.11.0 // indirect
70
+
go.uber.org/zap v1.26.0 // indirect
71
+
golang.org/x/crypto v0.32.0 // indirect
72
+
golang.org/x/net v0.33.0 // indirect
73
+
golang.org/x/sys v0.29.0 // indirect
74
+
golang.org/x/text v0.21.0 // indirect
75
+
golang.org/x/time v0.11.0 // indirect
76
+
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
77
+
lukechampine.com/blake3 v1.2.1 // indirect
78
+
)
+303
musicbrainz/go.sum
+303
musicbrainz/go.sum
···
1
+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2
+
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
3
+
github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e h1:yEW1njmALj7i1AjLhq6Lsxts48JUCTT+wpM9m7GNLVY=
4
+
github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng=
5
+
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
6
+
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
7
+
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
8
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
11
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
+
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
13
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
14
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
15
+
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
16
+
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
17
+
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
18
+
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
19
+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
20
+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
21
+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
22
+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
23
+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
24
+
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
25
+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
26
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
27
+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
28
+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
29
+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
30
+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
31
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
32
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
33
+
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
34
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
35
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
36
+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
37
+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
38
+
github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8=
39
+
github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8=
40
+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
41
+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
42
+
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
43
+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
44
+
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
45
+
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
46
+
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
47
+
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
48
+
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
49
+
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
50
+
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
51
+
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
52
+
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
53
+
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
54
+
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
55
+
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
56
+
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
57
+
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
58
+
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
59
+
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
60
+
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
61
+
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
62
+
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
63
+
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
64
+
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
65
+
github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk=
66
+
github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U=
67
+
github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg=
68
+
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
69
+
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
70
+
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
71
+
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
72
+
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
73
+
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
74
+
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
75
+
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
76
+
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
77
+
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
78
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
79
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
80
+
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
81
+
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
82
+
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
83
+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
84
+
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
85
+
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
86
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
87
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
88
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
89
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
90
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
91
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
92
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
93
+
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
94
+
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
95
+
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
96
+
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
97
+
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
98
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
99
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
100
+
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
101
+
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
102
+
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
103
+
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
104
+
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
105
+
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
106
+
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
107
+
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
108
+
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
109
+
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
110
+
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
111
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
112
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
113
+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
114
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
115
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
116
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
117
+
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
118
+
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
119
+
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
120
+
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
121
+
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
122
+
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
123
+
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
124
+
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
125
+
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
126
+
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
127
+
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
128
+
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
129
+
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
130
+
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
131
+
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
132
+
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
133
+
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
134
+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
135
+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
136
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
137
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
138
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
139
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
140
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
141
+
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
142
+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
143
+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
144
+
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
145
+
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
146
+
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
147
+
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
148
+
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
149
+
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
150
+
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
151
+
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
152
+
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
153
+
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
154
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
155
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
156
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
157
+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
158
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
159
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
160
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
161
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
162
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
163
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
164
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
165
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
166
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
167
+
github.com/teal-fm/piper v0.0.0-20251003185718-473cb159ecbe h1:tQAxIgPCFq+VNm4sCGBCIMmtPzwFzPjTUQnsYI7Dx/I=
168
+
github.com/teal-fm/piper v0.0.0-20251003185718-473cb159ecbe/go.mod h1:T4s0uuV1isetJS7RMSDrPUBUpgnfmLO1Sn1WeoYOMS8=
169
+
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
170
+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
171
+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
172
+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
173
+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
174
+
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
175
+
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
176
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
177
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
178
+
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
179
+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
180
+
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
181
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
182
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
183
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
184
+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
185
+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
186
+
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
187
+
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
188
+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
189
+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
190
+
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
191
+
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
192
+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
193
+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
194
+
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
195
+
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
196
+
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
197
+
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
198
+
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
199
+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
200
+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
201
+
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
202
+
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
203
+
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
204
+
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
205
+
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
206
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
207
+
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
208
+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
209
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
210
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
211
+
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
212
+
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
213
+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
214
+
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
215
+
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
216
+
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
217
+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
218
+
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
219
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
220
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
221
+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
222
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
223
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
224
+
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
225
+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
226
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
227
+
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
228
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
229
+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
230
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
231
+
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
232
+
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
233
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
234
+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
235
+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
236
+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
237
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
238
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
239
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
240
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
241
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
242
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
243
+
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
244
+
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
245
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
246
+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
247
+
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
248
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
249
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
250
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
251
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
252
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
253
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
254
+
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
255
+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
256
+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
257
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
258
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
259
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
260
+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
261
+
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
262
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
263
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
264
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
265
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
266
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
267
+
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
268
+
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
269
+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
270
+
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
271
+
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
272
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
273
+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
274
+
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
275
+
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
276
+
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
277
+
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
278
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
279
+
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
280
+
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
281
+
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
282
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
283
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
284
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
285
+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
286
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
287
+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
288
+
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
289
+
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
290
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
291
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
292
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
293
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
294
+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
295
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
296
+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
297
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
298
+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
299
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
300
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
301
+
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
302
+
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
303
+
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+89
musicbrainz/main.go
+89
musicbrainz/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"log"
7
+
"net/http"
8
+
"os"
9
+
"os/signal"
10
+
"time"
11
+
12
+
"github.com/labstack/echo/v4"
13
+
"github.com/teal-fm/piper/db"
14
+
"github.com/teal-fm/piper/models"
15
+
"github.com/teal-fm/piper/service/musicbrainz"
16
+
)
17
+
18
+
type Server struct {
19
+
mb *musicbrainz.MusicBrainzService
20
+
}
21
+
22
+
func main() {
23
+
dbPath := os.Getenv("DB_PATH")
24
+
if dbPath == "" {
25
+
dbPath = "./piper.db"
26
+
}
27
+
28
+
database, err := db.New(dbPath)
29
+
if err != nil {
30
+
log.Fatalf("Error connecting to database: %v", err)
31
+
}
32
+
33
+
if err := database.Initialize(); err != nil {
34
+
log.Fatalf("Error initializing database: %v", err)
35
+
}
36
+
37
+
srv := &Server{
38
+
mb: musicbrainz.NewMusicBrainzService(database),
39
+
}
40
+
41
+
e := echo.New()
42
+
43
+
e.POST("/search", srv.searchHandler)
44
+
e.POST("/hydrate", srv.hydrateHandler)
45
+
46
+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
47
+
defer stop()
48
+
49
+
go func() {
50
+
port := os.Getenv("PORT")
51
+
52
+
if port == "" {
53
+
port = "8088"
54
+
}
55
+
56
+
if err := e.Start(fmt.Sprintf(":%s", port)); err != nil && err != http.ErrServerClosed {
57
+
e.Logger.Fatal(err)
58
+
}
59
+
}()
60
+
61
+
<-ctx.Done() // wait for Ctrl+C
62
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
63
+
defer cancel()
64
+
_ = e.Shutdown(shutdownCtx)
65
+
}
66
+
67
+
func (s *Server) searchHandler(c echo.Context) error {
68
+
var req musicbrainz.SearchParams
69
+
70
+
if err := c.Bind(&req); err != nil {
71
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
72
+
}
73
+
74
+
resp, _ := s.mb.SearchMusicBrainz(c.Request().Context(), req)
75
+
76
+
return c.JSON(http.StatusOK, resp)
77
+
}
78
+
79
+
func (s *Server) hydrateHandler(c echo.Context) error {
80
+
var req models.Track
81
+
82
+
if err := c.Bind(&req); err != nil {
83
+
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
84
+
}
85
+
86
+
resp, _ := musicbrainz.HydrateTrack(s.mb, req)
87
+
88
+
return c.JSON(http.StatusOK, resp)
89
+
}
+2
-1
package.json
+2
-1
package.json
···
26
26
"dev:storage": "cargo run -p rocksky-storage --release -- serve",
27
27
"dev:webscrobbler": "cargo run -p rockskyd --release -- webscrobbler",
28
28
"dev:tracklist": "cargo run -p rockskyd --release -- tracklist",
29
-
"db:pgpull": "cargo run -p rockskyd --release -- pull && rm -f rocksky-analytics.ddb* rocksky-feed.ddb* && curl -o rocksky-analytics.ddb https://backup.rocksky.app/rocksky-analytics.ddb && curl -o rocksky-feed.ddb https://backup.rocksky.app/rocksky-feed.ddb"
29
+
"db:pgpull": "cargo run -p rockskyd --release -- pull && rm -f rocksky-analytics.ddb* rocksky-feed.ddb* && curl -o rocksky-analytics.ddb https://backup.rocksky.app/rocksky-analytics.ddb && curl -o rocksky-feed.ddb https://backup.rocksky.app/rocksky-feed.ddb",
30
+
"mb": "cd musicbrainz && go run main.go"
30
31
},
31
32
"workspaces": [
32
33
"apps/api",