Skip to content

Commit 45d9dbf

Browse files
Kenth Fagerlundclaude
andcommitted
feat(lyrics,art): real-time synced lyrics & multi-protocol terminal album art
Closes #23. Adds two new zero-dependency engine packages and wires them into the TUI. pkg/lyrics - LRCLIB exact lookup, LRCLIB search fallback, then NetEase. - LRC parser handling [mm:ss(.xx|.xxx)], multiple stamps per line and [offset:NNN]; ActiveIndex/Progress resolve the line for a playback offset. - SplitTrackTitle turns dirty ICY titles into artist/title, returning nothing for station names and advert slugs so no pointless lookup is made. - RAM LRU plus a disk cache under ~/.cache/halpradio/lyrics/, with negative caching so unmatched stations do not hammer the providers. pkg/art - Cover lookup from iTunes, Deezer, MusicBrainz/Cover Art Archive and optionally Last.fm, capped and validated before use. - Terminal renderers for the Kitty graphics protocol, iTerm2 inline images, Sixel, truecolor half-blocks and Braille, plus env-based auto-detection. - Every renderer returns exactly `rows` lines whose lipgloss.Width equals `cols`, so escape-sequence transports cannot shift the Bubble Tea layout. TUI integration - L opens a lyrics drawer that takes its own columns rather than overlapping the station list, auto-scrolling around the active line with a progress gauge; j/k scroll unsynced sheets and , / . nudge the sync, which persists. - A opens a floating cover art viewer; the drawer also carries a thumbnail when the terminal is tall enough to spare the rows. - Lookups run as tea.Cmds keyed on a station+track signature, so every play path is covered and late results for a superseded track are dropped. - Artwork is rasterised in the update loop on a new cover, a resize or a surface toggle, keeping component views pure; a closed surface emits the protocol's image-delete escape so Kitty placements do not linger. While in the analog tuner, L keeps sweeping the dial. That branch also had its step sign inverted, so a fast sweep right now raises the frequency. No new module dependencies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c9349bb commit 45d9dbf

45 files changed

Lines changed: 10071 additions & 29 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ halpradio/
4343
├── party/ # P2P mesh synchronized radio rooms, Argon2id/AES-256-GCM E2EE & protocol engine
4444
├── plugin/ # Wazero Wasm sandbox, capability permissions, host API, registry client
4545
├── radio/ # Store (bundled/local/favorites), Station struct, RadioBrowser HTTP client
46+
├── lyrics/ # LRCLIB & NetEase lyric providers, LRC parser, RAM + disk cache
47+
├── art/ # Cover art providers & Kitty/iTerm2/Sixel/half-block/Braille renderers
4648
├── theme/theme.go # Theme struct & color palettes (tokyonight, catppuccin, synthwave, nord, gruvbox, dracula)
4749
├── timer/ # Pomodoro focus interval engine, sleep timer with volume fade, OS event dispatcher
4850
├── ui/ # Model, Update loop, View orchestrator, keymaps
@@ -69,6 +71,10 @@ halpradio/
6971
- **Rule**: Never hardcode hex color strings (e.g. `#7aa2f7`) inside component files.
7072
- Always use active theme tokens provided by `m.theme` (e.g. `theme.Primary`, `theme.Secondary`, `theme.Border`, `theme.Playing`).
7173

74+
### 3b. Terminal Image Rendering (`pkg/art`)
75+
- Every renderer must return exactly `rows` lines whose `lipgloss.Width` equals `cols`. Escape-sequence transports (Kitty APC, iTerm2 OSC 1337, Sixel DCS) pad with spaces so Bubble Tea's layout arithmetic still holds.
76+
- **Rule**: Rasterise artwork in [`pkg/ui/update.go`](./pkg/ui/update.go) (on a new cover or a `tea.WindowSizeMsg`), never inside a component `View()`.
77+
7278
### 4. Error Handling & TUI Resilience
7379
- Audio stream errors or invalid URLs should update `player.Manager` status to `StatusError` or populate `lastError`.
7480
- **Rule**: Never call `panic()` or `os.Exit()` inside UI updates or stream handlers. The TUI must remain interactive even when a stream fails.

CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
- `pkg/player/player.go`: Multi-backend player manager (`mpv`, `vlc`, `ffplay`, etc.) + native Go fallback (`oto/v3` + `go-mp3`) and ICY stream metadata listener.
1717
- `pkg/radio/store.go`: Station catalog store (`bundled`, `local`, `favorites`), YAML/JSON persistence.
1818
- `pkg/radio/radiobrowser.go`: RadioBrowser HTTP search client.
19+
- `pkg/lyrics/`: LRCLIB + NetEase lyric providers, LRC timestamp parser, RAM/disk cache.
20+
- `pkg/art/`: cover art providers and terminal image renderers (Kitty, iTerm2, Sixel, half-block, Braille).
1921
- `pkg/theme/theme.go`: Theme definitions (`tokyonight`, `catppuccin`, `synthwave`, `nord`, `gruvbox`, `dracula`).
2022
- `pkg/timer/`: Pomodoro focus state machine, sleep timer countdown, and OS notification dispatcher.
2123
- `pkg/ui/model.go` & `update.go` & `view.go`: Bubble Tea Model, Update loop, View orchestrator.
22-
- `pkg/ui/components/`: Sub-views (`header`, `sidebar`, `stationlist`, `playerbar`, `statusbar`, `visualizer`, `modals`, `whichkey`).
23-
- `pkg/util/`: Path resolution (`~/.config/halpradio/`) and clipboard helper.
24+
- `pkg/ui/components/`: Sub-views (`header`, `sidebar`, `stationlist`, `playerbar`, `statusbar`, `visualizer`, `modals`, `whichkey`, `lyrics`, `art`).
25+
- `pkg/ui/nowplaying.go`: Lyric/artwork lookup commands, sync offset, and the artwork rasterisation step.
26+
- `pkg/util/`: Path resolution (`~/.config/halpradio/`, `~/.cache/halpradio/`) and clipboard helper.
2427

2528
## 🎨 Code Style & Architectural Constraints
2629
1. **Thread Safety**: Always protect shared state in `player.Manager` with `m.mu.Lock()` / `m.mu.Unlock()`.
@@ -29,3 +32,4 @@
2932
4. **Theme Tokens**: Never hardcode hex color strings in UI components. Use `theme.Primary`, `theme.Border`, `theme.Playing`, etc.
3033
5. **Resilience**: Never call `panic()` or `os.Exit()` on playback errors. Set `m.status = StatusError` and let the TUI inform the user gracefully.
3134
6. **Verification**: Always run `go test ./...` and `gofmt -s -w .` after making modifications.
35+
7. **Terminal Images**: Every `art.Renderer` protocol must return exactly `rows` lines whose `lipgloss.Width` equals `cols`, so escape-sequence transports cannot shift the surrounding layout. Rasterise artwork in `pkg/ui/update.go`, never inside a component `View()`.

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,65 @@ Internet radio ICY streams often emit dirty titles like:
322322

323323
---
324324

325+
## 📜 Real-Time Synced Karaoke Lyrics & Multi-Protocol Terminal Album Art
326+
327+
Modern terminals grew real graphics capabilities, so `halpradio` uses them. Press `L` for a live lyric sheet that scrolls itself, and `A` for the cover art of whatever is on air — all without leaving the terminal.
328+
329+
### 🎤 Live Synced Lyrics Drawer (`L` key)
330+
- **LRCLIB First**: Queries [LRCLIB](https://lrclib.net) with the artist, title and duration taken from ICY stream metadata or the acoustic fingerprint, then falls back to NetEase when LRCLIB has no match.
331+
- **Auto-Scrolling Karaoke View**: When timestamped `.lrc` data exists, the drawer highlights the line being sung, fades the surrounding lines, and draws a progress gauge across the active line.
332+
- **Manual Scroll For Plain Text**: Unsynced lyrics render as a formatted sheet you scroll with `j` / `k`.
333+
- **Sync Nudge**: Internet radio exposes no seek position, so the lyric clock starts when the station announces a new title. Press `,` and `.` to shift the sync in 0.5 second steps when a station announces late or early.
334+
- **Never Blocks The UI**: Every lookup runs as a Bubble Tea command off the update loop, so the TUI stays responsive on slow connections, and the drawer takes its own columns rather than overlapping the station list.
335+
- **Disk & Memory Cache**: Sheets are memoised in RAM and cached under `~/.cache/halpradio/lyrics/`, and stations with no match are negative-cached so the APIs are not hammered every track.
336+
337+
### 🖼️ Multi-Protocol Album Art (`A` key)
338+
`halpradio` detects your terminal's best image transport at startup and encodes artwork for it:
339+
340+
| Priority | Protocol | Terminals |
341+
|---|---|---|
342+
| 1 | **Kitty Graphics** | Ghostty, Kitty, WezTerm |
343+
| 2 | **iTerm2 Inline Images** | iTerm2, WezTerm |
344+
| 3 | **Sixel** | Foot, xterm, mlterm, yaft |
345+
| 4 | **Truecolor Half-Block** | every 24-bit colour terminal |
346+
| 5 | **Braille** | 256-colour and monochrome fallback |
347+
348+
- **High-Res Cover Lookup**: Artwork is resolved from the iTunes Search API, Deezer, MusicBrainz plus the [Cover Art Archive](https://coverartarchive.org/), and Last.fm when you supply `lastfm_api_key`.
349+
- **No Distortion On Basic Terminals**: The half-block and Braille renderers letterbox the image to keep covers square, and every renderer emits output padded to an exact cell grid so the surrounding layout never shifts.
350+
- **Two Surfaces**: A thumbnail sits at the top of the lyrics drawer, and `A` opens a floating full-size viewer showing the album, the provider and the active protocol.
351+
- **Cached Locally**: Downloaded covers live under `~/.cache/halpradio/art/`.
352+
353+
```text
354+
┌─ 📻 CATALOG ──────────────────┬─ 📜 LIVE LYRICS ───────────────┐
355+
│ ▶ SomaFM Groove Salad │ ▄▄▄▄▄▄▄▄▄▄▄▄ │
356+
│ Nightwave Plaza │ █ ALBUM ART █ │
357+
│ Radio Paradise │ ▀▀▀▀▀▀▀▀▀▀▀▀ │
358+
│ KEXP 90.3 │ 🖼 iTunes │
359+
│ │ Tycho - A Walk │
360+
│ │ │
361+
│ │ I've been wandering │
362+
│ │ ► Searching for a signal ◄ │
363+
│ │ Everything is quiet │
364+
│ │ ━━━━━━━━━━━─────── │
365+
│ │ ⏱ Synced via LRCLIB │
366+
│ │ L close · , . sync │
367+
└───────────────────────────────┴────────────────────────────────┘
368+
```
369+
370+
Tune the feature from `~/.config/halpradio/config.yaml`:
371+
```yaml
372+
lyrics_enabled: true # LRCLIB / NetEase synced lyrics engine
373+
lyrics_auto_open: false # open the drawer on startup
374+
lyrics_offset_ms: 0 # persistent sync correction
375+
album_art_enabled: true # terminal cover art renderer
376+
album_art_protocol: auto # auto | kitty | iterm2 | sixel | halfblock | braille | off
377+
lastfm_api_key: "" # optional extra cover art provider
378+
```
379+
380+
Set `HALPRADIO_NO_ART=1` to disable image rendering for a single run, or `HALPRADIO_ART_PROTOCOL=halfblock` to force a transport when detection guesses wrong.
381+
382+
---
383+
325384
## 🎉 Terminal Party Line: P2P Mesh Synchronized Radio Rooms & Reactions
326385

327386
Share the groove with teammates, study groups, or friends with zero central audio relaying! `halpradio` features an end-to-end encrypted (E2EE) P2P mesh party system powered by WebRTC data channels:
@@ -378,6 +437,9 @@ Press `?` or `F1` anywhere in **halpradio** to open the floating **WhichKey Over
378437
| **Discovery & Sharing** | `Ctrl+p` | Open **Party Room Manager** (P2P mesh synchronized listening & room setup) |
379438
| | `1` - `5` | Send live floating ASCII reaction (🔥 ❤️ ☕ 🚀 👀) when in Party Room |
380439
| | `I` | **Identify playing track** via acoustic stream fingerprinting (Chromaprint / AcoustID) |
440+
| | `L` | Toggle **live synced lyrics drawer** (LRCLIB / NetEase) |
441+
| | `A` | Toggle **album art viewer** (Kitty / Sixel / iTerm2 / half-block) |
442+
| | `,` / `.` | Nudge lyric sync backward / forward by 0.5s (lyrics drawer open) |
381443
| | `y` | Yank / copy track metadata (`Artist - Title`) or identified song to system clipboard |
382444
| | `o` | Open streaming search in default web browser (Spotify, YT Music, Apple, DDG, Google) |
383445
| | `s` | Star / bookmark track to `~/.config/halpradio/saved_tracks.txt` (on History tab) |
@@ -510,6 +572,7 @@ Explore detailed technical documentation in the [`docs/`](./docs) folder:
510572
- 🔌 **[Plugin & Extension System Guide](./docs/PLUGINS.md)**: Sandboxed WebAssembly (Wasm) architecture, capability permissions, developer SDK, and publishing to the official registry.
511573
- 🎵 **[Audio Engine & Stream Player](./docs/AUDIO_PLAYER.md)**: Multi-backend auto-detection (`mpv`, `vlc`, `ffplay`, native Go), process lifecycle, and real-time ICY metadata extraction.
512574
- 📻 **[Station Catalog & RadioBrowser Integration](./docs/STATION_MANAGEMENT.md)**: Station storage hierarchy (`stations.yaml`, local config, favorites), RadioBrowser API client, and PR export workflow.
575+
- 📜 **[Synced Lyrics & Terminal Album Art](./docs/LYRICS_AND_ART.md)**: LRCLIB / NetEase lyric providers, LRC parsing, playback-position estimation, cover art providers, and the Kitty / iTerm2 / Sixel / half-block / Braille renderers.
513576
- 🎨 **[Theme System & Audio Visualizers](./docs/THEME_SYSTEM.md)**: Lipgloss styling system, theme palettes, and TUI visualizer algorithms.
514577
- ⚙️ **[Configuration & Keybindings](./docs/CONFIGURATION.md)**: Directory layout, `config.yaml` options, CLI flags, and complete keymap reference.
515578
- 📦 **[Packaging & Distribution Guide](./docs/PACKAGING.md)**: Specifications for Homebrew, Arch Linux AUR, Docker, Scoop, and Nix.

docs/ARCHITECTURE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ halpradio/
5353
├── docs/ # Detailed technical documentation
5454
└── pkg/
5555
├── app/ # CLI flag parsing, configuration loading & app bootstrap
56+
├── art/ # Cover art providers plus Kitty / iTerm2 / Sixel / half-block / Braille renderers
57+
├── lyrics/ # LRCLIB & NetEase lyric providers, LRC parser, RAM and disk caches
5658
├── player/ # Multi-backend audio playback engine & ICY stream reader
5759
│ └── fingerprint/ # Acoustic stream recognition, Chromaprint / AcoustID client, LRU cache
5860
├── party/ # P2P mesh synchronized radio rooms, Argon2id/AES-256-GCM E2EE & protocol engine
@@ -61,7 +63,7 @@ halpradio/
6163
├── theme/ # Theme definitions & color palette registry
6264
├── timer/ # Pomodoro focus engine, sleep timer with volume fade, and OS event dispatcher
6365
├── ui/ # Main Bubble Tea Model, Update, View, and Keymap logic
64-
│ └── components/ # Modular UI sub-views (Header, StationList, PlayerBar, Visualizer, Modals, PartyBar)
66+
│ └── components/ # Modular UI sub-views (Header, StationList, PlayerBar, Visualizer, Modals, PartyBar, LyricsDrawer, AlbumArt)
6567
└── util/ # OS configuration directory resolution & clipboard utilities
6668
```
6769

@@ -71,6 +73,8 @@ halpradio/
7173
|---|---|---|
7274
| [`pkg/app`](../pkg/app/app.go) | `Run()`, `RunPluginCLI()` | Parses CLI flags (`--backend`, `--theme`, `--version`, `--fingerprint`, `--auto-identify`), handles CLI subcommands (`remote`, `plugin`, `party`), sets up store, instantiates `player.Manager`, initializes `tea.Program`. |
7375
| [`pkg/party`](../pkg/party/sync.go) | `Session`, `MeshNode`, `Packet`, `Crypto` | P2P mesh synchronized radio rooms, WebRTC data channels, Argon2id key derivation & AES-256-GCM encryption, sub-second playback sync, host election, ASCII reaction bus. |
76+
| [`pkg/art`](../pkg/art/client.go) | `Client`, `Cover`, `Renderer`, `Protocol` | Resolves high-resolution cover art from iTunes, Deezer, MusicBrainz / Cover Art Archive and Last.fm, then encodes it for the terminal's best image transport with RAM and disk caching. |
77+
| [`pkg/lyrics`](../pkg/lyrics/lrclib.go) | `Client`, `Sheet`, `Line`, `ParseLRC()` | Queries LRCLIB and falls back to NetEase, parses `.lrc` timestamps, resolves the active line for a playback offset, and caches sheets in RAM and on disk. |
7478
| [`pkg/player`](../pkg/player/player.go) | `Player`, `Manager`, `TrackInfo` | Detects audio CLI backends (`mpv`, `vlc`, `ffplay`, etc.) or falls back to native Go audio. Runs ICY metadata streaming goroutine. |
7579
| [`pkg/player/fingerprint`](../pkg/player/fingerprint/client.go) | `Client`, `Result`, `LRUCache` | Captures 5s audio buffers, computes Chromaprint subfingerprints, queries AcoustID & MusicBrainz APIs with LRU caching. |
7680
| [`pkg/plugin`](../pkg/plugin/manager.go) | `Manager`, `Sandbox`, `Manifest`, `RegistryClient` | Executes sandboxed WebAssembly plugins via Wazero with capability checks (`network`, `storage`, `events`). Fetches and verifies official registry packages. |

docs/CONFIGURATION.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ fingerprint_enabled: true # Enable on-demand acoustic recognition via 'I' (Chr
7070
auto_identify: false # Automatically identify music when streams lack ICY track metadata
7171
acoustid_api_key: "" # AcoustID API key (leave empty to use default halpradio client key)
7272

73+
# Synced Lyrics & Terminal Album Art
74+
lyrics_enabled: true # Enable the LRCLIB / NetEase synced lyrics drawer ('L' key)
75+
lyrics_auto_open: false # Open the lyrics drawer automatically on startup
76+
lyrics_offset_ms: 0 # Persistent lyric sync correction in milliseconds (',' / '.' adjust live)
77+
album_art_enabled: true # Enable terminal cover art rendering ('A' key)
78+
album_art_protocol: auto # auto | kitty | iterm2 | sixel | halfblock | braille | off
79+
lastfm_api_key: "" # Optional extra cover art provider (iTunes, Deezer & Cover Art Archive need no key)
80+
7381
# Experimental Features (On Hold)
7482
experimental_tuner: false # Enable experimental Analog Frequency Tuner on Tab 0 (see docs/TUNER.md)
7583
```

0 commit comments

Comments
 (0)