A decentralized music tracking and discovery platform built on AT Protocol 🎵

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.