Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
NEXT_PUBLIC_SPOTIFY_CLIENT_ID=your_client_id
# Copy this file to .env.local and paste your Spotify app's Client ID.
# Get one at https://developer.spotify.com/dashboard
#
# Either variable name works. NEXT_PUBLIC_ matches the old Next.js setup;
# SPOTIFY_CLIENT_ID is used on Vercel and other hosts.
NEXT_PUBLIC_SPOTIFY_CLIENT_ID=
82 changes: 82 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
test-and-build:
name: fmt, lint, test, build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install just
run: curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin

- name: Install npm dependencies
run: npm ci

- name: Install Tish (npm)
run: npm install -g @tishlang/tish@^1.10.0

- name: fmt-check (optional — skip if tish-fmt unavailable)
run: |
if command -v tish-fmt >/dev/null 2>&1; then just fmt-check; else echo "skip fmt-check"; fi

- name: lint (optional — skip if tish-lint unavailable)
run: |
if command -v tish-lint >/dev/null 2>&1; then just lint; else echo "skip lint"; fi

- name: Test juke-cards
run: just test-cards

- name: Build jukebox
run: just build

- name: Upload static site artifact
uses: actions/upload-artifact@v4
with:
name: jukebox-public
path: packages/jukebox/public

juke-cards-prerelease:
name: juke-cards prerelease tarball
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: test-and-build
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: "22"

- run: npm ci

- name: Build juke-cards
run: npm run build --workspace=@spacedevin/juke-cards

- name: Pack tarball
working-directory: packages/juke-cards
run: npm pack && mv *.tgz juke-cards-npm-package.tgz

- name: Upload prerelease asset
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=$(node -p "require('./packages/juke-cards/package.json').version")
TAG="v${VERSION}"
gh release upload "$TAG" packages/juke-cards/juke-cards-npm-package.tgz --clobber 2>/dev/null || \
gh release create "$TAG" packages/juke-cards/juke-cards-npm-package.tgz --prerelease --title "$TAG" --generate-notes
27 changes: 27 additions & 0 deletions .github/workflows/deploy-jukebox.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Deploy jukebox

on:
push:
branches: [main]
workflow_dispatch:

jobs:
deploy:
name: Vercel static deploy
runs-on: ubuntu-latest
if: vars.VERCEL_DEPLOY == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci && npm install -g @tishlang/tish@^1.10.0
- run: just build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: packages/jukebox
vercel-args: "--prod"
33 changes: 33 additions & 0 deletions .github/workflows/npm-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: NPM release (juke-cards)

on:
release:
types: [published]

jobs:
publish:
name: Publish @spacedevin/juke-cards
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
ref: refs/tags/${{ github.event.release.tag_name }}

- name: Download tarball from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ASSETS='${{ toJSON(github.event.release.assets) }}'
URL=$(echo "$ASSETS" | jq -r '.[] | select(.name == "juke-cards-npm-package.tgz") | .url')
test -n "$URL" && test "$URL" != "null"
curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/octet-stream" "$URL" -o juke-cards-npm-package.tgz

- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: https://registry.npmjs.org

- run: npm publish juke-cards-npm-package.tgz --access public
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ node_modules
*.log
.DS_Store
.claude
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
.dry-run
packages/*/dist
packages/juke-cards/lib
packages/jukebox/public/dist
target
Cargo.lock
96 changes: 61 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,84 @@

![Loaded jukebox](docs/screenshots/demo.gif)

3D Spotify jukebox modeled after a chrome-and-neon 50s diner record machine. Three.js scene, full procedural card art ([`@spacedevin/juke-cards`](packages/juke-cards)), real Spotify integration — your playlists become the records on the drum.

3D Spotify jukebox modeled after a chrome-and-neon 50s diner record machine. Three.js scene, full procedural card art, real Spotify integration — your playlists become the records on the drum.
Built as a **Tish + Lattish** static SPA (no Next.js server). Uses PKCE auth — each user authenticates with their own Spotify account and all API calls go directly from their browser to Spotify.

Uses PKCE auth — each user authenticates with their own Spotify account and all API calls go directly from their browser to Spotify (no shared rate limit on a server proxy).
## Monorepo

```
packages/
juke-cards/ @spacedevin/juke-cards — procedural canvas card art (npm)
jukebox/ Lattish SPA served from public/
bridges/ Three.js + Tone.js vendor bundle (JS shim)
```

## Setup

1. Create an app at https://developer.spotify.com/dashboard
2. Add redirect URIs (one per environment you'll run on):
- `https://juke.sh/callback` (juke server)
2. Add redirect URIs:
- `https://juke.sh/callback`
- `http://127.0.0.1:3000/callback` (local dev)
3. Copy `.env.local.example` to `.env.local`, fill in `NEXT_PUBLIC_SPOTIFY_CLIENT_ID`
4. `npm install && npm run dev`
5. Visit `http://127.0.0.1:3000` → click **LOGIN WITH SPOTIFY**
6. On the playlist picker, choose one or more playlists to load into the deck
3. `npm install`
4. Copy `.env.local.example` to `.env.local` and set `NEXT_PUBLIC_SPOTIFY_CLIENT_ID`
5. `just dev` — serves on http://127.0.0.1:3000
6. Visit `/` → **LOGIN WITH SPOTIFY** (Client ID is pre-filled from `.env.local`)
7. On `/box`, pick playlists to load into the deck

Playback requires an active Spotify device — open Spotify somewhere on your account (desktop app, phone, web player, etc.) so the API has something to send `play` / `pause` / `next` to.
No client secret needed — PKCE auth is browser-only. The Client ID is public and is baked into `public/dist/config.js` at build time (same as the old `NEXT_PUBLIC_` Next.js flow).

## Routes
For production (Vercel, etc.), set `NEXT_PUBLIC_SPOTIFY_CLIENT_ID` or `SPOTIFY_CLIENT_ID` in the host's environment variables at build time. You can still paste a Client ID on `/` instead — it is stored in `localStorage` and overrides the baked default.

Playback requires an active Spotify device (desktop app, phone, web player, etc.).

## Commands

| Command | Description |
|---------|-------------|
| `just dev` | Build + Tish dev server (`:3000`, SPA fallback) |
| `just build` | Cards + vendor + Lattish bundles → `packages/jukebox/public/dist/` |
| `just test` | juke-cards smoke tests |
| `just fmt` / `just lint` | Tish format/lint (requires `TISH_ROOT` cargo binaries) |

See [docs/TISH_TOOLING.md](docs/TISH_TOOLING.md) for compiler install and CI notes.

- `/` — landing page. Auto-redirects to `/box` if you're already signed in.
- `/box` — the jukebox app. Loads tokens from `localStorage`; if missing, shows just the login button (no full landing graphics).
- `/dev` — login-less visual playground with procedural fake tracks. Works in production too.
- `/callback` — OAuth redirect target.
## Local development

Arrow keys nudge the auto-rotation (←/→) and row count (↑/↓); `C` toggles category labels. Default is no auto-spin.
From the repo root:

Routes are case-insensitive via middleware (`/BOX`, `/Box`, etc. all 308 → `/box`).
```bash
npm install
cp .env.local.example .env.local # add your Spotify Client ID
just dev # http://127.0.0.1:3000 — register this callback URL in Spotify
```

## Controls
To test on a phone over the network, use a tunnel (e.g. `ngrok http 3000`) and add `https://YOUR-TUNNEL/callback` to your Spotify app's redirect URIs. The dev server logs OAuth hits on `/callback`.

- **Tap a card** — play that track on your active Spotify device.
- **Tap an already-playing card** — deselect.
- **Drag left/right** — spin the jukebox. Rotation speed auto-scales with the size of the deck so a 1000-track jukebox doesn't whip past you.
- **Drag up/down** — continuously blend between Roller-Rink (up) and Classic Diner (down) lighting.
- **Scroll wheel / pinch** — continuously zoom between fit-to-screen and tight-on-cards.
- **Double-tap empty chrome** — snap zoom between min and max.
- The currently playing track illuminates with edge LEDs + four tracker LEDs that converge along the top rim from opposite sides so you can always find your way home.
## Routes

- `/` — landing (Client ID + login)
- `/box` — jukebox app
- `/dev` — procedural debug tracks (no Spotify)
- `/callback` — OAuth redirect
- `/logout` — clear session

Arrow keys: spin (←/→), rows (↑/↓). `C` category labels, `M` audio, `S` shuffle, `Z`/`X` zoom tight, `V` telephoto flatness.

## Caching
## Deploy (Vercel)

- Selected playlists are saved to `localStorage` (`jukebox_selected_playlists`).
- Their assembled track lists are cached for 1 hour, keyed by the sorted set of selected IDs (`jukebox_tracks_cache`).
- The picker modal always shows on refresh with previous picks pre-selected; selecting the same set hits the cache, selecting a different set re-fetches.
- A green `● CACHED` chip appears in the picker next to any playlist whose tracks are currently cached. Click it to flush and re-fetch on next load.
This is a **static SPA**, not Next.js. In the Vercel project settings:

## Notes
1. **Root Directory:** leave empty (repo root) — or set `packages/jukebox` if you prefer; both `vercel.json` files are configured for monorepo builds.
2. **Framework Preset:** **Other** (not Next.js). The repo sets `"framework": null` in `vercel.json` to skip framework detection.
3. **Environment variables:** `NEXT_PUBLIC_SPOTIFY_CLIENT_ID` or `SPOTIFY_CLIENT_ID` (build time → `public/dist/config.js`).

- No client secret needed — PKCE auth is browser-only.
- Tokens stored in `localStorage`; access token refresh handled automatically.
- The `NEXT_PUBLIC_` prefix on the client ID means it ships in the JS bundle, which is fine — Spotify client IDs are public; only the secret would be sensitive (and PKCE doesn't use one).
- Spotify-owned algorithmic playlists (IDs starting with `37i9...` — Daily Mix, Discover Weekly, editorial picks) were locked down by Spotify in late 2024 and now return 404 to the Web API. The picker only shows playlists you own or follow, which is the correct accessible set.
Build runs `npm run vercel-build` from the monorepo root (installs workspaces, compiles Tish, writes static files to `packages/jukebox/public/`).

## Card art package

Procedural 256×100 slot cards live in [`@spacedevin/juke-cards`](packages/juke-cards). Demo grid: `packages/juke-cards/demo/index.html` (after `npm run build --workspace=@spacedevin/juke-cards`).

## License
PIF

PIF
4 changes: 0 additions & 4 deletions app/box/page.tsx

This file was deleted.

25 changes: 0 additions & 25 deletions app/callback/page.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions app/dev/page.tsx

This file was deleted.

Loading
Loading