A decentralized music tracking and discovery platform built on AT Protocol 馃幍
rocksky.app
spotify
atproto
lastfm
musicbrainz
scrobbling
listenbrainz
1import { Hono } from "hono";
2import { serveStatic } from "hono/bun";
3import { renderer } from "./renderer";
4import { TopArtistsEmbedPage } from "./embeds/TopArtistsEmbedPage";
5import { TopAlbumsEmbedPage } from "./embeds/TopAlbumsEmbedPage";
6import { TopTracksEmbedPage } from "./embeds/TopTracksEmbedPage";
7import { SongEmbedPage } from "./embeds/SongEmbedPage";
8import { ArtistEmbedPage } from "./embeds/ArtistEmbedPage";
9import { AlbumEmbedPage } from "./embeds/AlbumEmbedPage";
10import { ProfileEmbedPage } from "./embeds/ProfileEmbedPage";
11import { NowPlayingEmbedPage } from "./embeds/NowPlayingEmbedPage";
12import { RecentScrobblesEmbedPage } from "./embeds/RecentScrobblesEmbedPage";
13import { SummaryEmbedPage } from "./embeds/SummaryEmbedPage";
14import getProfile from "./xrpc/getProfile";
15import getProfileStats from "./xrpc/getStats";
16import getTopGenres from "./xrpc/getTopGenres";
17import getTopTrack from "./xrpc/getTopTrack";
18import getTopArtists from "./xrpc/getTopArtists";
19import getTopAlbums from "./xrpc/getTopAlbums";
20import getTopTracks from "./xrpc/getTopTracks";
21import getRecentScrobbles from "./xrpc/getRecentScrobbles";
22import chalk from "chalk";
23import { logger } from "hono/logger";
24import { ScrobbleEmbedPage } from "./embeds/ScrobbleEmbedPage";
25import getScrobble from "./xrpc/getScrobble";
26import { Embed } from "./embeds/Embed";
27
28const app = new Hono();
29
30app.use(logger());
31app.use("/public/*", serveStatic({ root: "./" }));
32app.use(renderer);
33
34app.get("/embed/u/:handle/top/artists", async (c) => {
35 const handle = c.req.param("handle");
36 const [{ profile, ok: profileOk }, { topArtists, ok: artistsOk }] =
37 await Promise.all([getProfile(handle), getTopArtists(handle)]);
38
39 if (!profileOk || !artistsOk) {
40 return c.text("Profile not found", 404);
41 }
42
43 return c.render(
44 <TopArtistsEmbedPage profile={profile} artists={topArtists} />,
45 );
46});
47
48app.get("/embed/u/:handle/top/albums", async (c) => {
49 const handle = c.req.param("handle");
50 const [{ profile, ok: profileOk }, { topAlbums, ok: albumsOk }] =
51 await Promise.all([getProfile(handle), getTopAlbums(handle)]);
52 if (!profileOk || !albumsOk) {
53 return c.text("Profile not found", 404);
54 }
55
56 return c.render(<TopAlbumsEmbedPage profile={profile} albums={topAlbums} />);
57});
58
59app.get("/embed/u/:handle/top/tracks", async (c) => {
60 const handle = c.req.param("handle");
61 const [{ profile, ok: profileOk }, { topTracks, ok: tracksOk }] =
62 await Promise.all([getProfile(handle), getTopTracks(handle)]);
63
64 if (!profileOk || !tracksOk) {
65 return c.text("Profile not found", 404);
66 }
67
68 return c.render(<TopTracksEmbedPage profile={profile} tracks={topTracks} />);
69});
70
71app.get("/embed/:did/song/:rkey", (c) => {
72 return c.render(<SongEmbedPage />);
73});
74
75app.get("/embed/:did/artist/:rkey", (c) => {
76 return c.render(<ArtistEmbedPage />);
77});
78
79app.get("/embed/:did/album/:rkey", (c) => {
80 return c.render(<AlbumEmbedPage />);
81});
82
83app.get("/embed/u/:handle", async (c) => {
84 const handle = c.req.param("handle");
85 const [
86 { profile, ok: profileOk },
87 { stats, ok: statsOk },
88 { genres, ok: genresOk },
89 { topTrack, ok: topTrackOk },
90 ] = await Promise.all([
91 getProfile(handle),
92 getProfileStats(handle),
93 getTopGenres(handle),
94 getTopTrack(handle),
95 ]);
96
97 if (!profileOk || !statsOk || !genresOk || !topTrackOk) {
98 return c.text("Profile not found", 404);
99 }
100
101 return c.render(
102 <ProfileEmbedPage
103 handle={profile.handle}
104 avatarUrl={profile.avatar || ""}
105 displayName={profile.displayName || ""}
106 stats={stats}
107 genres={genres}
108 topTrack={{
109 title: topTrack?.title || "Unknown Track",
110 artist: topTrack?.artist || "Unknown Artist",
111 albumArtist: topTrack?.albumArtist || "Unknown Album Artist",
112 trackUrl: `https://rocksky.app/${topTrack?.uri?.split("at://")[1]?.replace("app.rocksky.", "")}`,
113 artistUrl: `https://rocksky.app/${topTrack?.artistUri?.split("at://")[1]?.replace("app.rocksky.", "")}`,
114 albumCoverUrl: topTrack?.albumArt || "",
115 albumUrl: `https://rocksky.app/${topTrack?.albumUri?.split("at://")[1]?.replace("app.rocksky.", "")}`,
116 }}
117 />,
118 );
119});
120
121app.get("/embed/u/:handle/now", (c) => {
122 return c.render(<NowPlayingEmbedPage />);
123});
124
125app.get("/embed/u/:handle/recent", async (c) => {
126 const handle = c.req.param("handle");
127 const [{ profile, ok: profileOk }, { scrobbles, ok: scrobblesOk }] =
128 await Promise.all([getProfile(handle), getRecentScrobbles(handle)]);
129
130 if (!profileOk || !scrobblesOk) {
131 return c.text("Profile not found", 404);
132 }
133
134 return c.render(
135 <RecentScrobblesEmbedPage profile={profile} scrobbles={scrobbles} />,
136 );
137});
138
139app.get("/embed/u/:handle/summary", (c) => {
140 return c.render(<SummaryEmbedPage />);
141});
142
143app.get("/embed/:did/scrobble/:rkey", async (c) => {
144 const did = c.req.param("did");
145 const rkey = c.req.param("rkey");
146 const uri = `at://${did}/app.rocksky.scrobble/${rkey}`;
147 const [{ profile, ok: profileOk }, { scrobble, ok: scrobbleOk }] =
148 await Promise.all([getProfile(did), getScrobble(uri)]);
149
150 if (!scrobbleOk || !profileOk || !scrobble) {
151 return c.text("Scrobble not found", 404);
152 }
153
154 return c.render(<ScrobbleEmbedPage profile={profile} scrobble={scrobble} />);
155});
156
157app.get("/", (c) => {
158 return c.render(<Embed />);
159});
160
161console.log(
162 chalk.greenBright(`
163 ______ __ __
164 / ____/___ ___ / /_ ___ ____/ /
165 / __/ / __ \`__ \\/ __ \\/ _ \\/ __ /
166 / /___/ / / / / / /_/ / __/ /_/ /
167/_____/_/ /_/ /_/_.___/\\___/\\__,_/
168`),
169);
170
171const port = process.env.EMBED_PORT ? Number(process.env.EMBED_PORT) : 4001;
172console.log(
173 chalk.blueBright(
174 "馃殌 Server is running!" + chalk.whiteBright(` http://localhost:${port}`),
175 ),
176);
177
178export default {
179 port,
180 fetch: app.fetch,
181};