···1+import { TealContext } from "@/ctx";
2+import { db, tealSession, play } from "@teal/db";
3+import { eq, and } from "drizzle-orm";
4+import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getPlay";
5+6+export default async function getPlay(c: TealContext) {
7+ // do we have required query params?
8+ const params = c.req.query();
9+ if (params.authorDid === undefined) {
10+ throw new Error("authorDid is required");
11+ }
12+ if (!params.rkey) {
13+ throw new Error("rkey is required");
14+ }
15+16+ let res = await db
17+ .select()
18+ .from(play)
19+ .where(
20+ and(eq(play.authorDid, params.authorDid), and(eq(play.uri, params.rkey))),
21+ )
22+ .execute();
23+24+ if (res.length === 0) {
25+ throw new Error("Play not found");
26+ }
27+ res[0];
28+29+ // return a PlayView
30+ return {
31+ play: {
32+ uri: res[0].uri,
33+ authorDid: res[0].authorDid,
34+ createdAt: res[0].createdAt,
35+ indexedAt: res[0].indexedAt,
36+ trackName: res[0].trackName,
37+ trackMbId: res[0].trackMbId,
38+ recordingMbId: res[0].recordingMbId,
39+ duration: res[0].duration,
40+ artistNames: res[0].artistNames,
41+ artistMbIds: res[0].artistMbIds,
42+ releaseName: res[0].releaseName,
43+ releaseMbId: res[0].releaseMbId,
44+ isrc: res[0].isrc,
45+ originUrl: res[0].originUrl,
46+ musicServiceBaseDomain: res[0].musicServiceBaseDomain,
47+ submissionClientAgent: res[0].submissionClientAgent,
48+ playedTime: res[0].playedTime,
49+ },
50+ } as OutputSchema;
51+}
+16
apps/aqua/src/xrpc/route.ts
···0000000000000000
···1+import { EnvWithCtx } from "@/ctx";
2+import { Hono } from "hono";
3+import getPlay from "./feed/getPlay";
4+import getActorFeed from "./feed/getActorFeed";
5+6+// mount this on /xrpc
7+const app = new Hono<EnvWithCtx>();
8+9+app.get("fm.teal.alpha.getPlay", async (c) => c.json(await getPlay(c)));
10+app.get("fm.teal.alpha.feed.getActorFeed", async (c) =>
11+ c.json(await getActorFeed(c)),
12+);
13+14+export const getXrpcRouter = () => {
15+ return app;
16+};
+8-1
packages/db/schema.ts
···15 toDriver(value: TData): string {
16 return JSON.stringify(value);
17 },
000000018 })();
1920// Tables
···91 originUrl: text(),
92 /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. */
93 musicServiceBaseDomain: text(),
94- /** A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b */
95 submissionClientAgent: text(),
96 /** The unix timestamp of when the track was played */
97 playedTime: text(),
···15 toDriver(value: TData): string {
16 return JSON.stringify(value);
17 },
18+ // handle single value (no json array) as well
19+ fromDriver(value: string): TData {
20+ if (value[0] === "[") {
21+ return JSON.parse(value);
22+ }
23+ return [value] as TData;
24+ },
25 })();
2627// Tables
···98 originUrl: text(),
99 /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. */
100 musicServiceBaseDomain: text(),
101+ /** A user-agent style string specifying the user agent. e.g. fm.teal.frontend/0.0.1b */
102 submissionClientAgent: text(),
103 /** The unix timestamp of when the track was played */
104 playedTime: text(),
···67 },
68 "musicServiceBaseDomain": {
69 "type": "string",
70- "description": "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided."
71 },
72 "submissionClientAgent": {
73 "type": "string",
74 "maxLength": 256,
75 "maxGraphemes": 2560,
76- "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."
77 },
78 "playedTime": {
79 "type": "string",
···67 },
68 "musicServiceBaseDomain": {
69 "type": "string",
70+ "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."
71 },
72 "submissionClientAgent": {
73 "type": "string",
74 "maxLength": 256,
75 "maxGraphemes": 2560,
76+ "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."
77 },
78 "playedTime": {
79 "type": "string",
+8-3
packages/lexicons/src/lexicons.ts
···339 type: 'query',
340 parameters: {
341 type: 'params',
342- required: ['cursor'],
343 properties: {
344 authorDID: {
345 type: 'string',
···349 cursor: {
350 type: 'string',
351 description: 'The cursor to start the query from',
00000352 },
353 },
354 },
···481 musicServiceBaseDomain: {
482 type: 'string',
483 description:
484- "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided.",
485 },
486 submissionClientAgent: {
487 type: 'string',
488 maxLength: 256,
489 maxGraphemes: 2560,
490 description:
491- "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.",
492 },
493 playedTime: {
494 type: 'string',
···339 type: 'query',
340 parameters: {
341 type: 'params',
342+ required: ['authorDID'],
343 properties: {
344 authorDID: {
345 type: 'string',
···349 cursor: {
350 type: 'string',
351 description: 'The cursor to start the query from',
352+ },
353+ limit: {
354+ type: 'integer',
355+ description:
356+ 'The upper limit of tracks to get per request. Default is 20, max is 50.',
357 },
358 },
359 },
···486 musicServiceBaseDomain: {
487 type: 'string',
488 description:
489+ "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided.",
490 },
491 submissionClientAgent: {
492 type: 'string',
493 maxLength: 256,
494 maxGraphemes: 2560,
495 description:
496+ "A metadata string specifying the user agent. e.g. com.example.frontend/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if unavailable or not provided.",
497 },
498 playedTime: {
499 type: 'string',
···1112export interface QueryParams {
13 /** The author's DID for the play */
14- authorDID?: string
15 /** The cursor to start the query from */
16- cursor: string
0017}
1819export type InputSchema = undefined
···43 input: HandlerInput
44 req: express.Request
45 res: express.Response
046}
47export type Handler<HA extends HandlerAuth = never> = (
48 ctx: HandlerReqCtx<HA>,
···1112export 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}
2021export type InputSchema = undefined
···45 input: HandlerInput
46 req: express.Request
47 res: express.Response
48+ resetRouteRateLimits: () => Promise<void>
49}
50export type Handler<HA extends HandlerAuth = never> = (
51 ctx: HandlerReqCtx<HA>,
···27 isrc?: string
28 /** The URL associated with this track */
29 originUrl?: string
30- /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided. */
31 musicServiceBaseDomain?: string
32- /** 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. */
33 submissionClientAgent?: string
34 /** The unix timestamp of when the track was played */
35 playedTime?: string
···27 isrc?: string
28 /** The URL associated with this track */
29 originUrl?: string
30+ /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided. */
31 musicServiceBaseDomain?: string
32+ /** A metadata string specifying the user agent. e.g. com.example.frontend/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if unavailable or not provided. */
33 submissionClientAgent?: string
34 /** The unix timestamp of when the track was played */
35 playedTime?: string