A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add scrobble embed and embed improvements

Add ScrobbleEmbedPage and getScrobble xrpc, and register a new
/embed/:did/scrobble/:rkey route to render scrobbles with profile data.
Update Scrobble type (cover?, date) and change song/artist/album routes
to
use :did/:rkey params. Add various Tailwind utility classes and tweak
the
embed index layout to include a centered input. Add placeholder xrpc
stubs for album/artist/song.

+162 -13
+51
apps/embed/public/styles.css
··· 1349 1349 .m-\[20px\] { 1350 1350 margin: 20px; 1351 1351 } 1352 + .m-auto { 1353 + margin: auto; 1354 + } 1352 1355 .filter { 1353 1356 display: flex; 1354 1357 flex-wrap: wrap; ··· 1464 1467 .mt-\[-6px\] { 1465 1468 margin-top: -6px; 1466 1469 } 1470 + .mt-\[5px\] { 1471 + margin-top: 5px; 1472 + } 1467 1473 .mt-\[10px\] { 1468 1474 margin-top: 10px; 1475 + } 1476 + .mt-\[20px\] { 1477 + margin-top: 20px; 1469 1478 } 1470 1479 .mr-\[5px\] { 1471 1480 margin-right: 5px; ··· 1481 1490 } 1482 1491 .mr-\[25px\] { 1483 1492 margin-right: 25px; 1493 + } 1494 + .mb-\[5px\] { 1495 + margin-bottom: 5px; 1496 + } 1497 + .mb-\[10px\] { 1498 + margin-bottom: 10px; 1484 1499 } 1485 1500 .mb-\[15px\] { 1486 1501 margin-bottom: 15px; ··· 1727 1742 .max-h-\[18px\] { 1728 1743 max-height: 18px; 1729 1744 } 1745 + .max-h-\[20-px\] { 1746 + max-height: 20-px; 1747 + } 1730 1748 .max-h-\[20px\] { 1731 1749 max-height: 20px; 1732 1750 } ··· 1747 1765 } 1748 1766 .max-h-\[100px\] { 1749 1767 max-height: 100px; 1768 + } 1769 + .max-h-\[200px\] { 1770 + max-height: 200px; 1771 + } 1772 + .max-h-\[250px\] { 1773 + max-height: 250px; 1750 1774 } 1751 1775 .min-h-screen { 1752 1776 min-height: 100vh; ··· 1779 1803 outline-style: none; 1780 1804 } 1781 1805 } 1806 + .w-1\/2 { 1807 + width: calc(1/2 * 100%); 1808 + } 1809 + .w-1\/3 { 1810 + width: calc(1/3 * 100%); 1811 + } 1782 1812 .w-\[30px\] { 1783 1813 width: 30px; 1784 1814 } 1785 1815 .w-\[60px\] { 1786 1816 width: 60px; 1817 + } 1818 + .w-fit { 1819 + width: fit-content; 1787 1820 } 1788 1821 .w-full { 1789 1822 width: 100%; ··· 1811 1844 } 1812 1845 .max-w-\[100px\] { 1813 1846 max-width: 100px; 1847 + } 1848 + .max-w-\[200px\] { 1849 + max-width: 200px; 1850 + } 1851 + .max-w-\[250px\] { 1852 + max-width: 250px; 1814 1853 } 1815 1854 .max-w-full { 1816 1855 max-width: 100%; ··· 1911 1950 } 1912 1951 .rounded-\[5px\] { 1913 1952 border-radius: 5px; 1953 + } 1954 + .rounded-\[8px\] { 1955 + border-radius: 8px; 1914 1956 } 1915 1957 .rounded-box { 1916 1958 border-radius: var(--radius-box); ··· 2341 2383 .\!bg-base-100 { 2342 2384 background-color: var(--color-base-100) !important; 2343 2385 } 2386 + .bg-\[\#00fff3\] { 2387 + background-color: #00fff3; 2388 + } 2344 2389 .bg-\[var\(--color-avatar-background\)\] { 2345 2390 background-color: var(--color-avatar-background); 2346 2391 } ··· 2365 2410 } 2366 2411 .pr-\[15px\] { 2367 2412 padding-right: 15px; 2413 + } 2414 + .text-center { 2415 + text-align: center; 2368 2416 } 2369 2417 .align-bottom { 2370 2418 vertical-align: bottom; ··· 2401 2449 } 2402 2450 .whitespace-nowrap { 2403 2451 white-space: nowrap; 2452 + } 2453 + .text-\[\#ff2876\] { 2454 + color: #ff2876; 2404 2455 } 2405 2456 .text-\[var\(--color-genre\)\] { 2406 2457 color: var(--color-genre);
+1 -1
apps/embed/src/embeds/AlbumEmbedPage.tsx
··· 1 1 export function AlbumEmbedPage() { 2 - return <></>; 2 + return <>Album</>; 3 3 }
+1 -1
apps/embed/src/embeds/ArtistEmbedPage.tsx
··· 1 1 export function ArtistEmbedPage() { 2 - return <></>; 2 + return <>Artist</>; 3 3 }
+1 -1
apps/embed/src/embeds/NowPlayingEmbedPage.tsx
··· 1 1 export function NowPlayingEmbedPage() { 2 - return <></>; 2 + return <>Now Playing</>; 3 3 }
+58
apps/embed/src/embeds/ScrobbleEmbedPage.tsx
··· 1 + import dayjs from "dayjs"; 2 + import type { Profile } from "../types/profile"; 3 + import type { Scrobble } from "../types/scrobble"; 4 + 5 + export type ScrobbleEmbedPageProps = { 6 + profile: Profile; 7 + scrobble: Scrobble; 8 + }; 9 + 10 + export function ScrobbleEmbedPage(props: ScrobbleEmbedPageProps) { 11 + console.log("ScrobbleEmbedPage props:", props); 12 + return ( 13 + <div className="p-[15px] flex items-center justify-center"> 14 + <div className=""> 15 + <a 16 + href={`https://rocksky.app/${props.scrobble.uri.split("at://")[1]?.replace("app.rocksky.", "")}`} 17 + className=" no-underline" 18 + target="_blank" 19 + > 20 + <img 21 + className="max-h-[250px] max-w-[250px] rounded-[8px] mb-[5px]" 22 + src={props.scrobble.cover} 23 + /> 24 + </a> 25 + <a 26 + href={`https://rocksky.app/${props.scrobble.uri.split("at://")[1]?.replace("app.rocksky.", "")}`} 27 + className="text-inherit no-underline" 28 + target="_blank" 29 + > 30 + <div>{props.scrobble.title}</div> 31 + </a> 32 + <div className="text-black bg-[#00fff3] w-fit"> 33 + {props.scrobble.artist} 34 + </div> 35 + <div className="flex items-center mt-[10px]"> 36 + <a> 37 + <img 38 + src={props.profile.avatar} 39 + className="max-h-[25px] max-w-[25px] rounded-full mr-[10px]" 40 + /> 41 + </a> 42 + 43 + <a 44 + href={`https://rocksky.app/profile/${props.profile.handle}`} 45 + className="text-[#ff2876] no-underline" 46 + target="_blank" 47 + > 48 + @{props.profile.handle} 49 + </a> 50 + </div> 51 + <div className="-[14px]">played this song</div> 52 + <div className="font-rockford-light text-[var(--color-text-muted)] text-[14px]"> 53 + {dayjs(props.scrobble.date).format("MMM D, YYYY [at] h:mm A")} 54 + </div> 55 + </div> 56 + </div> 57 + ); 58 + }
+1 -1
apps/embed/src/embeds/SongEmbedPage.tsx
··· 1 1 export function SongEmbedPage() { 2 - return <></>; 2 + return <>Song</>; 3 3 }
+30 -9
apps/embed/src/index.tsx
··· 21 21 import getRecentScrobbles from "./xrpc/getRecentScrobbles"; 22 22 import chalk from "chalk"; 23 23 import { logger } from "hono/logger"; 24 + import { ScrobbleEmbedPage } from "./embeds/ScrobbleEmbedPage"; 25 + import getScrobble from "./xrpc/getScrobble"; 24 26 25 27 const app = new Hono(); 26 28 ··· 65 67 return c.render(<TopTracksEmbedPage profile={profile} tracks={topTracks} />); 66 68 }); 67 69 68 - app.get("/embed/song/:id", (c) => { 70 + app.get("/embed/:did/song/:rkey", (c) => { 69 71 return c.render(<SongEmbedPage />); 70 72 }); 71 73 72 - app.get("/embed/artist/:id", (c) => { 74 + app.get("/embed/:did/artist/:rkey", (c) => { 73 75 return c.render(<ArtistEmbedPage />); 74 76 }); 75 77 76 - app.get("/embed/album/:id", (c) => { 78 + app.get("/embed/:did/album/:rkey", (c) => { 77 79 return c.render(<AlbumEmbedPage />); 78 80 }); 79 81 ··· 137 139 return c.render(<SummaryEmbedPage />); 138 140 }); 139 141 142 + app.get("/embed/:did/scrobble/:rkey", async (c) => { 143 + const did = c.req.param("did"); 144 + const rkey = c.req.param("rkey"); 145 + const uri = `at://${did}/app.rocksky.scrobble/${rkey}`; 146 + const [{ profile, ok: profileOk }, { scrobble, ok: scrobbleOk }] = 147 + await Promise.all([getProfile(did), getScrobble(uri)]); 148 + 149 + if (!scrobbleOk || !profileOk || !scrobble) { 150 + return c.text("Scrobble not found", 404); 151 + } 152 + 153 + return c.render(<ScrobbleEmbedPage profile={profile} scrobble={scrobble} />); 154 + }); 155 + 140 156 app.get("/", (c) => { 141 157 return c.render( 142 - <div className="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center"> 143 - <div className="bg-white p-8 rounded-lg shadow-2xl"> 144 - <h1 className="text-4xl font-bold text-gray-800 mb-4"> 158 + <div className="min-h-screen w-1/3 flex items-center justify-center m-auto"> 159 + <div className="w-full"> 160 + <h1 className="text-4xl font-bold text-gray-800 mb-4 text-center"> 145 161 Embed a Rocksky Scrobble 146 162 </h1> 147 - <p className="text-gray-600"> 148 - This is server-side rendered with Tailwind CSS 149 - </p> 163 + <div> 164 + <input 165 + type="text" 166 + className="input w-full" 167 + aria-label="input" 168 + placeholder="https://rocksky.app/did:plc:7vdlgi2bflelz7mmuxoqjfcr/scrobble/3mdt3zncfoc23" 169 + /> 170 + </div> 150 171 </div> 151 172 </div>, 152 173 );
+2
apps/embed/src/types/scrobble.ts
··· 5 5 artist: string; 6 6 albumArtist: string; 7 7 albumArt: string; 8 + cover?: string; 8 9 album: string; 9 10 handle: string; 10 11 did: string; ··· 13 14 artistUri: string; 14 15 albumUri: string; 15 16 createdAt: string; 17 + date: string; 16 18 };
apps/embed/src/xrpc/getAlbum.ts

This is a binary file and will not be displayed.

apps/embed/src/xrpc/getArtist.ts

This is a binary file and will not be displayed.

+17
apps/embed/src/xrpc/getScrobble.ts
··· 1 + import { ROCKSKY_API_URL } from "../consts"; 2 + import type { Scrobble } from "../types/scrobble"; 3 + 4 + export default async function getScrobble(uri: string) { 5 + const url = new URL( 6 + `${ROCKSKY_API_URL}/xrpc/app.rocksky.scrobble.getScrobble`, 7 + ); 8 + url.searchParams.append("uri", uri); 9 + const res = await fetch(url); 10 + 11 + if (!res.ok) { 12 + return { scrobble: null, ok: res.ok }; 13 + } 14 + 15 + const scrobble = (await res.json()) as Scrobble; 16 + return { scrobble, ok: res.ok }; 17 + }
apps/embed/src/xrpc/getSong.ts

This is a binary file and will not be displayed.