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

### 3b. Terminal Image Rendering (`pkg/art`)
- 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.
- **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()`.

### 4. Error Handling & TUI Resilience
- Audio stream errors or invalid URLs should update `player.Manager` status to `StatusError` or populate `lastError`.
- **Rule**: Never call `panic()` or `os.Exit()` inside UI updates or stream handlers. The TUI must remain interactive even when a stream fails.
Expand Down
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
- `pkg/player/player.go`: Multi-backend player manager (`mpv`, `vlc`, `ffplay`, etc.) + native Go fallback (`oto/v3` + `go-mp3`) and ICY stream metadata listener.
- `pkg/radio/store.go`: Station catalog store (`bundled`, `local`, `favorites`), YAML/JSON persistence.
- `pkg/radio/radiobrowser.go`: RadioBrowser HTTP search client.
- `pkg/lyrics/`: LRCLIB + NetEase lyric providers, LRC timestamp parser, RAM/disk cache.
- `pkg/art/`: cover art providers and terminal image renderers (Kitty, iTerm2, Sixel, half-block, Braille).
- `pkg/theme/theme.go`: Theme definitions (`tokyonight`, `catppuccin`, `synthwave`, `nord`, `gruvbox`, `dracula`).
- `pkg/timer/`: Pomodoro focus state machine, sleep timer countdown, and OS notification dispatcher.
- `pkg/ui/model.go` & `update.go` & `view.go`: Bubble Tea Model, Update loop, View orchestrator.
- `pkg/ui/components/`: Sub-views (`header`, `sidebar`, `stationlist`, `playerbar`, `statusbar`, `visualizer`, `modals`, `whichkey`).
- `pkg/util/`: Path resolution (`~/.config/halpradio/`) and clipboard helper.
- `pkg/ui/components/`: Sub-views (`header`, `sidebar`, `stationlist`, `playerbar`, `statusbar`, `visualizer`, `modals`, `whichkey`, `lyrics`, `art`).
- `pkg/ui/nowplaying.go`: Lyric/artwork lookup commands, sync offset, and the artwork rasterisation step.
- `pkg/util/`: Path resolution (`~/.config/halpradio/`, `~/.cache/halpradio/`) and clipboard helper.

## 🎨 Code Style & Architectural Constraints
1. **Thread Safety**: Always protect shared state in `player.Manager` with `m.mu.Lock()` / `m.mu.Unlock()`.
Expand All @@ -29,3 +32,4 @@
4. **Theme Tokens**: Never hardcode hex color strings in UI components. Use `theme.Primary`, `theme.Border`, `theme.Playing`, etc.
5. **Resilience**: Never call `panic()` or `os.Exit()` on playback errors. Set `m.status = StatusError` and let the TUI inform the user gracefully.
6. **Verification**: Always run `go test ./...` and `gofmt -s -w .` after making modifications.
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()`.
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,66 @@ Internet radio ICY streams often emit dirty titles like:

---

## 📜 Real-Time Synced Karaoke Lyrics & Multi-Protocol Terminal Album Art

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.

### 🎤 Live Synced Lyrics Drawer (`L` key)
- **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.
- **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.
- **Manual Scroll For Plain Text**: Unsynced lyrics render as a formatted sheet you scroll with `j` / `k`.
- **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.
- **Never Blocks The UI**: Every lookup runs as a Bubble Tea command off the update loop, so the TUI stays responsive on slow connections.
- **Fits Any Terminal**: At 80 columns or wider the drawer takes its own columns rather than overlapping the station list; below that the sheet becomes a full-width overlay, and resizing moves it between the two without closing it.
- **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.

### 🖼️ Multi-Protocol Album Art (`A` key)
`halpradio` detects your terminal's best image transport at startup and encodes artwork for it:

| Priority | Protocol | Terminals |
|---|---|---|
| 1 | **Kitty Graphics** | Ghostty, Kitty, WezTerm |
| 2 | **iTerm2 Inline Images** | iTerm2, WezTerm |
| 3 | **Sixel** | Foot, xterm, mlterm, yaft |
| 4 | **Truecolor Half-Block** | every 24-bit colour terminal |
| 5 | **Braille** | 256-colour and monochrome fallback |

- **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`.
- **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.
- **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.
- **Cached Locally**: Downloaded covers live under `~/.cache/halpradio/art/`.

```text
┌─ 📻 CATALOG ──────────────────┬─ 📜 LIVE LYRICS ───────────────┐
│ ▶ SomaFM Groove Salad │ ▄▄▄▄▄▄▄▄▄▄▄▄ │
│ Nightwave Plaza │ █ ALBUM ART █ │
│ Radio Paradise │ ▀▀▀▀▀▀▀▀▀▀▀▀ │
│ KEXP 90.3 │ 🖼 iTunes │
│ │ Tycho - A Walk │
│ │ │
│ │ I've been wandering │
│ │ ► Searching for a signal ◄ │
│ │ Everything is quiet │
│ │ ━━━━━━━━━━━─────── │
│ │ ⏱ Synced via LRCLIB │
│ │ L close · , . sync │
└───────────────────────────────┴────────────────────────────────┘
```

Tune the feature from `~/.config/halpradio/config.yaml`:
```yaml
lyrics_enabled: true # LRCLIB / NetEase synced lyrics engine
lyrics_auto_open: false # open the drawer on startup
lyrics_offset_ms: 0 # persistent sync correction
album_art_enabled: true # terminal cover art renderer
album_art_protocol: auto # auto | kitty | iterm2 | sixel | halfblock | braille | off
lastfm_api_key: "" # optional extra cover art provider
```

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.

---

## 🎉 Terminal Party Line: P2P Mesh Synchronized Radio Rooms & Reactions

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:
Expand Down Expand Up @@ -378,6 +438,9 @@ Press `?` or `F1` anywhere in **halpradio** to open the floating **WhichKey Over
| **Discovery & Sharing** | `Ctrl+p` | Open **Party Room Manager** (P2P mesh synchronized listening & room setup) |
| | `1` - `5` | Send live floating ASCII reaction (🔥 ❤️ ☕ 🚀 👀) when in Party Room |
| | `I` | **Identify playing track** via acoustic stream fingerprinting (Chromaprint / AcoustID) |
| | `L` | Toggle **live synced lyrics drawer** (LRCLIB / NetEase) |
| | `A` | Toggle **album art viewer** (Kitty / Sixel / iTerm2 / half-block) |
| | `,` / `.` | Nudge lyric sync backward / forward by 0.5s (lyrics drawer open) |
| | `y` | Yank / copy track metadata (`Artist - Title`) or identified song to system clipboard |
| | `o` | Open streaming search in default web browser (Spotify, YT Music, Apple, DDG, Google) |
| | `s` | Star / bookmark track to `~/.config/halpradio/saved_tracks.txt` (on History tab) |
Expand Down Expand Up @@ -510,6 +573,7 @@ Explore detailed technical documentation in the [`docs/`](./docs) folder:
- 🔌 **[Plugin & Extension System Guide](./docs/PLUGINS.md)**: Sandboxed WebAssembly (Wasm) architecture, capability permissions, developer SDK, and publishing to the official registry.
- 🎵 **[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.
- 📻 **[Station Catalog & RadioBrowser Integration](./docs/STATION_MANAGEMENT.md)**: Station storage hierarchy (`stations.yaml`, local config, favorites), RadioBrowser API client, and PR export workflow.
- 📜 **[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.
- 🎨 **[Theme System & Audio Visualizers](./docs/THEME_SYSTEM.md)**: Lipgloss styling system, theme palettes, and TUI visualizer algorithms.
- ⚙️ **[Configuration & Keybindings](./docs/CONFIGURATION.md)**: Directory layout, `config.yaml` options, CLI flags, and complete keymap reference.
- 📦 **[Packaging & Distribution Guide](./docs/PACKAGING.md)**: Specifications for Homebrew, Arch Linux AUR, Docker, Scoop, and Nix.
Expand Down
6 changes: 5 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ halpradio/
├── docs/ # Detailed technical documentation
└── pkg/
├── app/ # CLI flag parsing, configuration loading & app bootstrap
├── art/ # Cover art providers plus Kitty / iTerm2 / Sixel / half-block / Braille renderers
├── lyrics/ # LRCLIB & NetEase lyric providers, LRC parser, RAM and disk caches
├── player/ # Multi-backend audio playback engine & ICY stream reader
│ └── fingerprint/ # Acoustic stream recognition, Chromaprint / AcoustID client, LRU cache
├── party/ # P2P mesh synchronized radio rooms, Argon2id/AES-256-GCM E2EE & protocol engine
Expand All @@ -61,7 +63,7 @@ halpradio/
├── theme/ # Theme definitions & color palette registry
├── timer/ # Pomodoro focus engine, sleep timer with volume fade, and OS event dispatcher
├── ui/ # Main Bubble Tea Model, Update, View, and Keymap logic
│ └── components/ # Modular UI sub-views (Header, StationList, PlayerBar, Visualizer, Modals, PartyBar)
│ └── components/ # Modular UI sub-views (Header, StationList, PlayerBar, Visualizer, Modals, PartyBar, LyricsDrawer, AlbumArt)
└── util/ # OS configuration directory resolution & clipboard utilities
```

Expand All @@ -71,6 +73,8 @@ halpradio/
|---|---|---|
| [`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`. |
| [`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. |
| [`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. |
| [`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. |
| [`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. |
| [`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. |
| [`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. |
Expand Down
8 changes: 8 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ fingerprint_enabled: true # Enable on-demand acoustic recognition via 'I' (Chr
auto_identify: false # Automatically identify music when streams lack ICY track metadata
acoustid_api_key: "" # AcoustID API key (leave empty to use default halpradio client key)

# Synced Lyrics & Terminal Album Art
lyrics_enabled: true # Enable the LRCLIB / NetEase synced lyrics drawer ('L' key)
lyrics_auto_open: false # Open the lyrics drawer automatically on startup
lyrics_offset_ms: 0 # Persistent lyric sync correction in milliseconds (',' / '.' adjust live)
album_art_enabled: true # Enable terminal cover art rendering ('A' key)
album_art_protocol: auto # auto | kitty | iterm2 | sixel | halfblock | braille | off
lastfm_api_key: "" # Optional extra cover art provider (iTunes, Deezer & Cover Art Archive need no key)

# Experimental Features (On Hold)
experimental_tuner: false # Enable experimental Analog Frequency Tuner on Tab 0 (see docs/TUNER.md)
```
Expand Down
Loading
Loading