···11import { TealContext } from "@/ctx";
22-import { db, tealSession, play } from "@teal/db";
33-import { eq, and } from "drizzle-orm";
44-import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getPlay";
22+import { db, plays, playToArtists, artists } from "@teal/db";
33+import { eq, and, lt, desc, sql } from "drizzle-orm";
44+import { OutputSchema } from "@teal/lexicons/src/types/fm/teal/alpha/feed/getActorFeed";
5566-export default async function getPlay(c: TealContext) {
77- // do we have required query params?
66+export default async function getActorFeed(c: TealContext) {
87 const params = c.req.query();
99- if (params.authorDid === undefined) {
88+ if (!params.authorDid) {
109 throw new Error("authorDid is required");
1110 }
1211 if (!params.rkey) {
1312 throw new Error("rkey is required");
1413 }
15141616- let res = await db
1717- .select()
1818- .from(play)
1919- .where(
2020- and(eq(play.authorDid, params.authorDid), and(eq(play.uri, params.rkey))),
1515+ // Get plays with artists as arrays
1616+ const playRes = await db
1717+ .select({
1818+ play: plays,
1919+ artists: sql<Array<{ mbid: string; name: string }>>`
2020+ COALESCE(
2121+ array_agg(
2222+ CASE WHEN ${artists.mbid} IS NOT NULL THEN
2323+ jsonb_build_object(
2424+ 'mbid', ${artists.mbid},
2525+ 'name', ${artists.name}
2626+ )
2727+ END
2828+ ) FILTER (WHERE ${artists.mbid} IS NOT NULL),
2929+ ARRAY[]::jsonb[]
3030+ )
3131+ `.as("artists"),
3232+ })
3333+ .from(plays)
3434+ .leftJoin(playToArtists, sql`${plays.uri} = ${playToArtists.playUri}`)
3535+ .leftJoin(artists, sql`${playToArtists.artistMbid} = ${artists.mbid}`)
3636+ .where(and(eq(plays.did, params.authorDid), eq(plays.rkey, params.rkey)))
3737+ .groupBy(
3838+ plays.uri,
3939+ plays.cid,
4040+ plays.did,
4141+ plays.duration,
4242+ plays.isrc,
4343+ plays.musicServiceBaseDomain,
4444+ plays.originUrl,
4545+ plays.playedTime,
4646+ plays.processedTime,
4747+ plays.rkey,
4848+ plays.recordingMbid,
4949+ plays.releaseMbid,
5050+ plays.releaseName,
5151+ plays.submissionClientAgent,
5252+ plays.trackName,
2153 )
2222- .execute();
5454+ .orderBy(desc(plays.playedTime))
5555+ .limit(1);
23562424- if (res.length === 0) {
5757+ if (playRes.length === 0) {
2558 throw new Error("Play not found");
2659 }
2727- res[0];
28602929- // return a PlayView
3061 return {
3131- play: {
3232- uri: res[0].uri,
3333- authorDid: res[0].authorDid,
3434- createdAt: res[0].createdAt,
3535- indexedAt: res[0].indexedAt,
3636- trackName: res[0].trackName,
3737- trackMbId: res[0].trackMbId,
3838- recordingMbId: res[0].recordingMbId,
3939- duration: res[0].duration,
4040- artistNames: res[0].artistNames,
4141- artistMbIds: res[0].artistMbIds,
4242- releaseName: res[0].releaseName,
4343- releaseMbId: res[0].releaseMbId,
4444- isrc: res[0].isrc,
4545- originUrl: res[0].originUrl,
4646- musicServiceBaseDomain: res[0].musicServiceBaseDomain,
4747- submissionClientAgent: res[0].submissionClientAgent,
4848- playedTime: res[0].playedTime,
4949- },
6262+ plays: playRes.map(({ play, artists }) => {
6363+ const {
6464+ uri,
6565+ did: authorDid,
6666+ processedTime: createdAt,
6767+ processedTime: indexedAt,
6868+ trackName,
6969+ cid: trackMbId,
7070+ cid: recordingMbId,
7171+ duration,
7272+ rkey,
7373+ releaseName,
7474+ cid: releaseMbId,
7575+ isrc,
7676+ originUrl,
7777+ musicServiceBaseDomain,
7878+ submissionClientAgent,
7979+ playedTime,
8080+ } = play;
8181+8282+ return {
8383+ uri,
8484+ authorDid,
8585+ createdAt: createdAt?.toISOString(),
8686+ indexedAt: indexedAt?.toISOString(),
8787+ trackName,
8888+ trackMbId,
8989+ recordingMbId,
9090+ duration,
9191+ // Replace these with actual artist data from the array
9292+ artistNames: artists.map((artist) => artist.name),
9393+ artistMbIds: artists.map((artist) => artist.mbid),
9494+ // Or, if you want to keep the full artist objects:
9595+ // artists: artists,
9696+ releaseName,
9797+ releaseMbId,
9898+ isrc,
9999+ originUrl,
100100+ musicServiceBaseDomain,
101101+ submissionClientAgent,
102102+ playedTime: playedTime?.toISOString(),
103103+ };
104104+ }),
50105 } as OutputSchema;
51106}
+1-1
apps/aqua/src/xrpc/route.ts
···66// mount this on /xrpc
77const app = new Hono<EnvWithCtx>();
8899-app.get("fm.teal.alpha.getPlay", async (c) => c.json(await getPlay(c)));
99+app.get("fm.teal.alpha.feed.getPlay", async (c) => c.json(await getPlay(c)));
1010app.get("fm.teal.alpha.feed.getActorFeed", async (c) =>
1111 c.json(await getActorFeed(c)),
1212);
+46
packages/db/.drizzle/0000_perfect_war_machine.sql
···11+CREATE TABLE "artists" (
22+ "mbid" uuid PRIMARY KEY NOT NULL,
33+ "name" text NOT NULL,
44+ "play_count" integer DEFAULT 0
55+);
66+--> statement-breakpoint
77+CREATE TABLE "play_to_artists" (
88+ "play_uri" text NOT NULL,
99+ "artist_mbid" uuid NOT NULL,
1010+ "artist_name" text,
1111+ CONSTRAINT "play_to_artists_play_uri_artist_mbid_pk" PRIMARY KEY("play_uri","artist_mbid")
1212+);
1313+--> statement-breakpoint
1414+CREATE TABLE "plays" (
1515+ "uri" text PRIMARY KEY NOT NULL,
1616+ "did" text NOT NULL,
1717+ "rkey" text NOT NULL,
1818+ "cid" text NOT NULL,
1919+ "isrc" text,
2020+ "duration" integer,
2121+ "track_name" text NOT NULL,
2222+ "played_time" timestamp with time zone,
2323+ "processed_time" timestamp with time zone DEFAULT now(),
2424+ "release_mbid" uuid,
2525+ "release_name" text,
2626+ "recording_mbid" uuid,
2727+ "submission_client_agent" text,
2828+ "music_service_base_domain" text
2929+);
3030+--> statement-breakpoint
3131+CREATE TABLE "recordings" (
3232+ "mbid" uuid PRIMARY KEY NOT NULL,
3333+ "name" text NOT NULL,
3434+ "play_count" integer DEFAULT 0
3535+);
3636+--> statement-breakpoint
3737+CREATE TABLE "releases" (
3838+ "mbid" uuid PRIMARY KEY NOT NULL,
3939+ "name" text NOT NULL,
4040+ "play_count" integer DEFAULT 0
4141+);
4242+--> statement-breakpoint
4343+ALTER TABLE "play_to_artists" ADD CONSTRAINT "play_to_artists_play_uri_plays_uri_fk" FOREIGN KEY ("play_uri") REFERENCES "public"."plays"("uri") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
4444+ALTER TABLE "play_to_artists" ADD CONSTRAINT "play_to_artists_artist_mbid_artists_mbid_fk" FOREIGN KEY ("artist_mbid") REFERENCES "public"."artists"("mbid") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
4545+ALTER TABLE "plays" ADD CONSTRAINT "plays_release_mbid_releases_mbid_fk" FOREIGN KEY ("release_mbid") REFERENCES "public"."releases"("mbid") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
4646+ALTER TABLE "plays" ADD CONSTRAINT "plays_recording_mbid_recordings_mbid_fk" FOREIGN KEY ("recording_mbid") REFERENCES "public"."recordings"("mbid") ON DELETE no action ON UPDATE no action;
-17
packages/db/.drizzle/0000_same_maelstrom.sql
···11-CREATE TABLE `auth_session` (
22- `key` text PRIMARY KEY NOT NULL,
33- `session` text NOT NULL
44-);
55---> statement-breakpoint
66-CREATE TABLE `auth_state` (
77- `key` text PRIMARY KEY NOT NULL,
88- `state` text NOT NULL
99-);
1010---> statement-breakpoint
1111-CREATE TABLE `status` (
1212- `uri` text PRIMARY KEY NOT NULL,
1313- `authorDid` text NOT NULL,
1414- `status` text NOT NULL,
1515- `createdAt` text NOT NULL,
1616- `indexedAt` text NOT NULL
1717-);
-3
packages/db/.drizzle/0001_fresh_tana_nile.sql
···11-ALTER TABLE `status` RENAME COLUMN "authorDid" TO "author_did";--> statement-breakpoint
22-ALTER TABLE `status` RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
33-ALTER TABLE `status` RENAME COLUMN "indexedAt" TO "indexed_at";
+6
packages/db/.drizzle/0001_swift_maddog.sql
···11+CREATE MATERIALIZED VIEW "public"."mv_artist_play_counts" AS (select "artists"."mbid", "artists"."name", count("plays"."uri") as "play_count" from "artists" left join "play_to_artists" on "artists"."mbid" = "play_to_artists"."artist_mbid" left join "plays" on "plays"."uri" = "play_to_artists"."play_uri" group by "artists"."mbid", "artists"."name");--> statement-breakpoint
22+CREATE MATERIALIZED VIEW "public"."mv_global_play_count" AS (select count("uri") as "total_plays", count(distinct "did") as "unique_listeners" from "plays");--> statement-breakpoint
33+CREATE MATERIALIZED VIEW "public"."mv_recording_play_counts" AS (select "recordings"."mbid", "recordings"."name", count("plays"."uri") as "play_count" from "recordings" left join "plays" on "plays"."recording_mbid" = "recordings"."mbid" group by "recordings"."mbid", "recordings"."name");--> statement-breakpoint
44+CREATE MATERIALIZED VIEW "public"."mv_release_play_counts" AS (select "releases"."mbid", "releases"."name", count("plays"."uri") as "play_count" from "releases" left join "plays" on "plays"."release_mbid" = "releases"."mbid" group by "releases"."mbid", "releases"."name");--> statement-breakpoint
55+CREATE MATERIALIZED VIEW "public"."mv_top_artists_30days" AS (select "artists"."mbid", "artists"."name", count("plays"."uri") as "play_count" from "artists" inner join "play_to_artists" on "artists"."mbid" = "play_to_artists"."artist_mbid" inner join "plays" on "plays"."uri" = "play_to_artists"."play_uri" where "plays"."played_time" >= NOW() - INTERVAL '30 days' group by "artists"."mbid", "artists"."name" order by count("plays"."uri") DESC);--> statement-breakpoint
66+CREATE MATERIALIZED VIEW "public"."mv_top_releases_30days" AS (select "releases"."mbid", "releases"."name", count("plays"."uri") as "play_count" from "releases" inner join "plays" on "plays"."release_mbid" = "releases"."mbid" where "plays"."played_time" >= NOW() - INTERVAL '30 days' group by "releases"."mbid", "releases"."name" order by count("plays"."uri") DESC);
-1
packages/db/.drizzle/0002_moaning_roulette.sql
···11-ALTER TABLE `auth_session` RENAME TO `atp_session`;
···11-CREATE TABLE `teal_session` (
22- `key` text PRIMARY KEY NOT NULL,
33- `session` text NOT NULL,
44- `provider` text NOT NULL
55-);
66---> statement-breakpoint
77-CREATE TABLE `teal_user` (
88- `did` text PRIMARY KEY NOT NULL,
99- `handle` text NOT NULL,
1010- `email` text NOT NULL,
1111- `created_at` text NOT NULL
1212-);
+15
packages/db/.drizzle/0003_worried_unicorn.sql
···11+CREATE TABLE "profiles" (
22+ "did" text PRIMARY KEY NOT NULL,
33+ "display_name" text NOT NULL,
44+ "description" text NOT NULL,
55+ "description_facets" jsonb NOT NULL,
66+ "avatar" text NOT NULL,
77+ "banner" text NOT NULL,
88+ "created_at" timestamp NOT NULL
99+);
1010+--> statement-breakpoint
1111+CREATE TABLE "featured_items" (
1212+ "did" text PRIMARY KEY NOT NULL,
1313+ "mbid" text NOT NULL,
1414+ "type" text NOT NULL
1515+);
-29
packages/db/.drizzle/0004_exotic_ironclad.sql
···11-CREATE TABLE `follow` (
22- `follower` text PRIMARY KEY NOT NULL,
33- `followed` text NOT NULL,
44- `created_at` text NOT NULL
55-);
66---> statement-breakpoint
77-CREATE TABLE `play` (
88- `uri` text PRIMARY KEY NOT NULL,
99- `author_did` text NOT NULL,
1010- `created_at` text NOT NULL,
1111- `indexed_at` text NOT NULL,
1212- `track_name` text NOT NULL,
1313- `track_mb_id` text,
1414- `recording_mb_id` text,
1515- `duration` integer,
1616- `artist_name` text NOT NULL,
1717- `artist_mb_ids` text,
1818- `release_name` text,
1919- `release_mb_id` text,
2020- `isrc` text,
2121- `origin_url` text,
2222- `music_service_base_domain` text,
2323- `submission_client_agent` text,
2424- `played_time` text
2525-);
2626---> statement-breakpoint
2727-ALTER TABLE `teal_user` ADD `avatar` text NOT NULL;--> statement-breakpoint
2828-ALTER TABLE `teal_user` ADD `bio` text;--> statement-breakpoint
2929-ALTER TABLE `teal_user` DROP COLUMN `email`;
+7
packages/db/.drizzle/0004_furry_gravity.sql
···11+ALTER TABLE "profiles" ALTER COLUMN "display_name" DROP NOT NULL;--> statement-breakpoint
22+ALTER TABLE "profiles" ALTER COLUMN "description" DROP NOT NULL;--> statement-breakpoint
33+ALTER TABLE "profiles" ALTER COLUMN "description_facets" DROP NOT NULL;--> statement-breakpoint
44+ALTER TABLE "profiles" ALTER COLUMN "avatar" DROP NOT NULL;--> statement-breakpoint
55+ALTER TABLE "profiles" ALTER COLUMN "banner" DROP NOT NULL;--> statement-breakpoint
66+ALTER TABLE "profiles" ALTER COLUMN "created_at" DROP NOT NULL;--> statement-breakpoint
77+ALTER TABLE "profiles" ADD COLUMN "handle" text;