my record collection, in 3d.
Rycord turns my Discogs collection into a cache-first 3D record room. Albums sit on a Kallax-style shelf in a rainy cafe scene, with real cover art, procedural spines, a turntable, and an RGB strip controlled by the remote on the floor.
This is built for a single personal collection, so upstream data is treated as
local library data once fetched. Cache files are plain JSON or images under
data/, which makes them easy to mount in Docker, edit by hand, or back up.
npm install
cp .env.example .env.local
npm run devThe dev server runs at http://localhost:3030.
Set DISCOGS_USER in .env.local to load a different public collection. Add
DISCOGS_TOKEN if the collection is private or you want authenticated Discogs
requests. If Discogs is unavailable and no cache exists yet, Rycord falls back
to bundled synthetic records.
See .env.example for the full commented list.
| var | required | purpose |
|---|---|---|
DISCOGS_USER |
no | Discogs username for the shelf |
DISCOGS_TOKEN |
no | Personal token for authenticated requests |
RYCORD_HIDDEN_RELEASE_IDS |
no | Comma/space-separated release IDs to hide |
RYCORD_ADMIN_PASSWORD |
no | Password for hidden settings-cog admin tools |
RYCORD_REFRESH_TOKEN |
no | Secret required for manual Discogs refreshes |
RYCORD_DATA_DIR |
no | Cache root, defaults to ./data |
LASTFM_API_KEY |
no | Description fallback after Wikipedia |
OPENROUTER_API_KEY |
no | Final AI fallback for album descriptions |
OPENROUTER_MODEL |
no | Comma-separated OpenRouter model chain |
MULTIPLY_RECORDS |
no | Dev-only shelf density test multiplier |
Normal app loads read local cache first. Rycord only calls upstream APIs when a needed cache file does not exist yet.
| path | contents |
|---|---|
data/collections/*.json |
Normalized collection entries and raw Discogs rows |
data/hidden-releases.json |
Optional display blacklist of Discogs release IDs |
data/releases/*.json |
Release metadata, notes, tracklist, durations, raw payload |
data/descriptions/*.json |
Wikipedia, Last.fm, or OpenRouter description payloads |
data/palettes/*.json |
Derived cover palettes |
data/covers/* |
Proxied cover image binaries and metadata |
Description lookup order is:
- Wikipedia
- Last.fm
- OpenRouter
Delete an individual cache file to force Rycord to fetch that item again.
To keep a release tracked in Discogs but hide it from Rycord, add its Discogs release ID to either:
RYCORD_HIDDEN_RELEASE_IDS=12345,67890or create data/hidden-releases.json:
["12345", "67890"]The filter applies to both the collection shelf and wantlist mode.
Rycord does not auto-refresh Discogs data. Normal app loads use the local cache
forever unless refresh=1 is passed with RYCORD_ADMIN_PASSWORD or
RYCORD_REFRESH_TOKEN.
You can also double-click the settings cog, enter RYCORD_ADMIN_PASSWORD, and
refresh the collection or wantlist from the hidden admin panel.
Use refresh=1 when you intentionally want to re-pull your Discogs collection:
curl -H "Authorization: Bearer YOUR_ADMIN_PASSWORD" \
"http://localhost:3030/api/collection?user=YOUR_DISCOGS_USER&refresh=1"Refresh the wantlist the same way:
curl -H "Authorization: Bearer YOUR_ADMIN_PASSWORD" \
"http://localhost:3030/api/wantlist?user=YOUR_DISCOGS_USER&refresh=1"The collection request rewrites data/collections/<user>.json, compares old cached
release IDs against the current Discogs collection, and prunes albums you no
longer own from:
data/releasesdata/descriptionsdata/palettesdata/covers
The response includes source and prunedReleaseIds.
Mount data/ into the container so the collection survives rebuilds:
volumes:
- ./data:/app/dataIf you want the cache somewhere else locally, set RYCORD_DATA_DIR.
npm run dev: Next dev server on port3030npm run build: production buildnpm start: run the production server on port3030npm run typecheck: TypeScript check
app/
page.tsx Load records and palettes, then mount SceneLoader
layout.tsx Root metadata and global styles
api/
collection/route.ts Collection response, refresh, and prune endpoint
cover/[id]/route.ts Same-origin cached Discogs cover proxy
release/[id]/route.ts Cached release details and tracklists
description/[id]/route.ts Cached Wikipedia, Last.fm, OpenRouter descriptions
components/
Scene.tsx R3F room, shelf layout, camera, post effects
VinylRecord.tsx Pull-out record interaction and front/back jacket
InfoPanel.tsx Active record metadata, notes, tracklist, actions
Room.tsx Floor, wall, baseboard, rug
RoomProps.tsx Wall art, plant, book stack, mug, other decor
RGBStrip.tsx Addressable LED strip
Remote.tsx In-scene controls for the LED strip
Paper.tsx Pickup-style note from me on the floor
Turntable.tsx Centerpiece player
Shelf.tsx Procedural Kallax-style grid
lib/
dataCache.ts Disk cache helpers
discogs.ts Discogs client, collection cache, refresh pruning
cachedAssets.ts Cover and palette disk cache
palette.ts Sharp-based palette extraction
releaseDetails.ts Client-side release detail loader
covers.ts Synthetic fallback records
ledStore.ts Shared state for the remote, paper, and LED strip
- Desktop-first scene, with
MobileGatefor narrow or touch viewports - One configured collection at a time
- One room theme
- No audio playback
I know the commit history is short - I have a bad habit of writing wild commit messages on projects when I don't think they'll go live. This one turned out nicely though, and I wanted to share it. Thus, I went ahead and squashed the commits down to just the few you see. Apologies if you wanted to see my process of iterating for some reason. Check my other projects out for that.