A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
at main 181 lines 5.8 kB view raw
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};