A vintage cassette deck for archive.org → your network audio. Browse the Live Music Archive, build mix tapes from arbitrary tracks, and stream the result to a Lyrion player, a DLNA renderer, an AirPlay-style endpoint, or your local Mac speakers — all from a Sony CHF90-styled UI that knows it's pretending to be hardware.
Tape · stry — woven from the live music archive.
The archive.org plugin bundled with Lyrion is a favorites browser, not a search tool. You have to find shows in a separate browser window, add them to your archive.org favorites, and only then browse them inside Lyrion. Tapestry skips that loop and stops being Lyrion-only on the way:
- Type a query (
pink floyd 1970). - Pick a result, hit ▸ Load tape — or ▤ Grab to save it for later, or click ▤ on individual tracks to build a mix tape.
- Press PLAY on the cassette deck.
- Audio streams directly from archive.org to whatever you've selected as the player — Lyrion, DLNA, your Mac, etc.
- archive.org search with year, format (FLAC / any), and source filters:
LIVE(curated etree archive plus title-heuristic concert/bootleg matches across the whole catalog),ALBUMS,RADIO,ALL. Noise collections (audiobooks, news scanners, religious radio) excluded by default. - Match artist only checkbox — pins the query to the
creatorfield so "Traffic" returns Traffic the band, not a traffic report.
- Multiple backends, one UI — pick a player from a single dropdown:
- Lyrion — JSON-RPC to your LMS instance (auto-discovered via mDNS, or set the URL in Settings).
- DLNA / UPnP — auto-discovered AVTransport renderers (ultraRendu in MPD/DLNA mode, Sonos, smart TVs, AVRs).
- This Mac — in-app playback via the system audio output.
- AirPlay coming.
- Hand-off — switching the player dropdown ejects from the old deck and re-loads the same tape on the new one, paused. Press PLAY to resume on the new device.
- Smart transport — REW restarts the current track if you're more than 3 s in, otherwise jumps to the previous; FF jumps to the next track but suppresses Lyrion's "wrap to same track" loop on single-item queues. Double-click on REW or FF seeks ±30 s within the current track.
- REC — dubs the currently-playing track onto a mix tape under construction.
- ▤ Grab — saves a tape to the drawer (from search results or via the Grab button on the loaded cassette's J-card insert).
- Click a tape in the drawer to "flip the case open" — full track list, with per-track ▸ play / ⤓ play next / + add to queue / ▤ add to mix, plus a single ▸ Load whole tape action.
- Sort — saved newest/oldest, title A→Z, artist A→Z, year newest/oldest.
- Color and cover art — each tape gets a deterministic cassette palette (band, paper, ink, font); when there's archive.org artwork, the dominant color is extracted (canvas + HSL bucketing) and overrides the deterministic colors so the spine matches the album. The deck cassette displays the same palette as the drawer spine for the loaded tape.
- ▤ on any track (search results or open drawer cases) adds it to a mix tape under construction.
- Bottom-of-screen tray persists across the deck, search, and drawer — keep adding tracks from any source.
- 90-minute cap, with a progress bar that goes red when full. Overflow is rejected.
- ▸ Record opens a save modal: name field, optional cover image upload (jpg/png/gif/webp, ≤ 6 MB) — uploaded covers are stored locally and their dominant color drives the mix tape's palette.
- Sony CHF90-styled cassette deck — three-section paper label (handwritten J-card with real track titles parsed from item descriptions), recessed bay with reels that grow/shrink with album playback, brushed-champagne chassis, piano-key transports, four-digit tape counter that ticks smoothly via predicted time between status polls.
- Album artwork backdrop — when a tape is loaded, archive.org's artwork (proxied to dodge CORS) is blurred + warm-tinted behind the cassette in the bay.
- Audio feedback — clunk on key press, cassette-load whoosh, eject spring — synthesized via Web Audio, no asset files.
- Keyboard shortcuts —
/search ·tdrawer ·rrec ·eeject ·Escclose
- Drawer + settings live at
~/Library/Application Support/Tapestry/(or./data/in dev). Mix-tape covers undermix-covers/. Settings include the LMS URL and resolved-source hints (env / settings / default).
- Backend — FastAPI + httpx (async). One Python module per backend (
players/lyrion.py,players/dlna.py,players/local.py); the unified API is/api/players/{backend}/{player_id}/{action}. - Frontend — vanilla ES module + hand-rolled CSS. No build step, no framework.
- Discovery — mDNS (
zeroconf) for LMS, SSDP (async-upnp-client) for DLNA. - Storage — JSON files for drawer + settings.
- Desktop shell — pywebview-based native window, packaged as
Tapestry.appvia PyInstaller.
Requires Python 3.11 or newer. macOS ships 3.9 by default — install via Homebrew (brew install python@3.13) if needed.
git clone https://github.com/Ethros19/tapestry.git
cd tapestry
python3.13 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# dev mode — auto-reloads on Python edits, hard-refresh for CSS/JS
uvicorn app.main:app --reload --port 8080
# → http://localhost:8080
# native window mode
python -m app.desktopThe app finds your LMS automatically the first time you open Settings (⚙) and click ↻ Find LMS, or set LYRION_URL once and forget it.
| Env var | Default | Purpose |
|---|---|---|
LYRION_URL |
http://192.168.1.xx:yyyy/jsonrpc.js |
JSON-RPC endpoint of your LMS — substitute your LMS host's LAN IP and port (default LMS port is 9000). Overridden by the Settings panel. |
TAPESTRY_DATA_DIR |
~/Library/Application Support/Tapestry/ (or ./data/ in dev) |
Where the drawer, settings, and mix-tape covers live. |
The Settings panel (⚙ in the topbar) also exposes:
- ↻ Find LMS — mDNS-discover Lyrion servers on the LAN.
- ↻ Rescan players — force backends with cached discovery (DLNA) to redo their search.
- ↻ Refresh artwork colors — re-extract dominant colors from each saved tape's archive.org artwork or uploaded cover.
The repo includes scripts to produce a fully self-contained macOS app and disk image:
pip install -r requirements-dev.txt # picks up Pillow + PyInstaller
./scripts/build-icon.sh # → dist/icon.icns (only run once, or after editing the icon)
./scripts/build-app.sh # → dist/Tapestry.app
./scripts/build-dmg.sh # → dist/Tapestry.dmg
open dist/Tapestry.appscripts/build-icon.py draws the cassette icon at 1024×1024 using Pillow primitives — edit the function and rerun if you want to riff on the design.
The unsigned .app triggers macOS Gatekeeper. Recipients need to right-click the app → Open → confirm in the dialog (or: System Settings → Privacy & Security → Open Anyway) the first time. After that it's trusted forever.
For a clean handoff, sign + notarize the build:
- Get an Apple Developer ID (
Developer ID Applicationcert). - Uncomment the
codesign/xcrun notarytoolblocks in bothscripts/build-app.shandscripts/build-dmg.sh, fill inDEVELOPER_IDand anotarytoolkeychain profile name. - Re-run the build.
Tapestry
┌─────────────────────┐
archive.org ──── search ──→│ FastAPI on :8080 │ ──── unified API ────→ ┬─ Lyrion (JSON-RPC)
│ + static frontend │ ├─ DLNA (UPnP/AVTransport)
└─────────────────────┘ ├─ Local (HTML <audio>)
└─ AirPlay (planned)
The audio bytes generally flow archive.org → player directly (LMS or a DLNA renderer fetches the URL it's told to play). Tapestry orchestrates control and metadata, not bytes.
All /api/players/{backend}/{player_id}/... endpoints accept these {action}s: status (GET), play, add, insert, play_show, load_show, start, pause, stop, next, prev, eject, seek_by, volume. The frontend calls the unified path; the legacy /api/lyrion/* is kept for backwards compatibility.
Other endpoints:
GET /api/players— aggregated list of players from every backend, with per-backenderrors.POST /api/players/rescan— force DLNA re-discovery.GET /api/lyrion/discover— mDNS-find LMS instances.GET /api/search?q=&year=&fmt=&source=&creator_only=GET /api/item/<identifier>GET /api/artwork/<identifier>— proxies archive.org artwork (CORS shim).GET /POST /api/drawer,DELETE /api/drawer/<identifier>POST /api/mix-cover— upload a cover image (multipart).GET /POST /api/settings
OpenAPI docs at /docs while the server is running.
A launchd plist at ~/Library/LaunchAgents/com.ethros.tapestry.plist with RunAtLoad=true and KeepAlive=true works fine — point its WorkingDirectory at the cloned repo and its ProgramArguments at the venv's uvicorn. Useful if you'd rather keep Tapestry running and access it from any browser on your network.
Search. LIVE filter is on by default — etree archive plus title-heuristic concert/bootleg matches across the whole catalog, with audiobook / radio-news collections explicitly excluded. The "Match artist only" toggle pins the query to the creator field for paranoid filtering.
Drawer. Each saved tape is a horizontal cassette spine — band color, paper, ink and font are all derived deterministically from the item's identifier (or extracted from the album artwork when available, so the spine matches the cover).
Drawer · case open. Click a spine and the case "flips open": album art, full track list, per-track ▸ play / ⤓ play next / + add to queue / ▤ add to mix, plus a single ▸ Load whole tape action. Building a mix tape from your saved drawer is faster than searching twice.
Want help installing, running, or extending Tapestry via Claude / Cursor / Aider / ChatGPT? Drop AI-HANDOFF.md into the agent's context first — it's a single-file primer (architecture, file layout, common tasks, gotchas, code style) that lets an AI start being useful immediately without you having to explain the project.
"Read AI-HANDOFF.md from the Tapestry repo, then help me add an AirPlay backend."
MIT.
- archive.org Live Music Archive — the corpus
- Lyrion Music Server — the player network
async-upnp-client— DLNA controlzeroconf— mDNS discovery- The Sony CHF90 — the visual reference
- The bootleg-trader culture that built etree.org and the tape-tree distribution method this app's branding nods to



