Skip to content

Latest commit

 

History

History
112 lines (89 loc) · 9.12 KB

File metadata and controls

112 lines (89 loc) · 9.12 KB

Architecture Overview 🏗️

halpradio is a keyboard-driven, terminal-based Internet Radio streaming application built in Go using the Bubble Tea TUI framework and Lipgloss styling library.


📐 Application Design Pattern (Elm Architecture)

halpradio strictly follows the Model-View-Update (MVU) pattern mandated by Bubble Tea:

flowchart TD
    User([User Keyboard / Mouse]) -->|Keypress / Mouse Event| Update[Update Loop `pkg/ui/update.go`]
    AsyncEvents([Async Events: ICY Metadata / API]) -->|Tea Cmd / Msg| Update
    Update -->|State Mutation| Model[Application State `pkg/ui/model.go`]
    Model -->|Render Layout| View[View Orchestrator `pkg/ui/view.go`]
    View -->|Lipgloss Styled Output| Terminal([Terminal Screen])

    Update -->|Player Actions| PlayerMgr[Player Manager `pkg/player/player.go`]
    PlayerMgr -->|ICY Metadata Callback| AsyncEvents
Loading

Core Components of MVU:

  1. Model (pkg/ui/model.go)
    Houses all application state including:

    • Active tab index (1: Activities, 2: Catalog, 3: Countries, 4: Genres, 5: Favorites, 6: RadioBrowser, 7: Custom, 8: History, 9: Globe, 0: Tuner)
    • Station catalog & store handle (pkg/radio/store.go)
    • Audio player handle (pkg/player/player.go)
    • UI focus states (FocusMainList, FocusSidebar), list cursor selections, and search queries
    • Active modals (Theme picker, Add station modal, WhichKey overlay, PR export modal)
    • Current theme token definition (pkg/theme/theme.go)
  2. Update (pkg/ui/update.go)
    Processes user inputs (LazyVim keybindings, search input) and asynchronous messages:

    • TrackUpdatedMsg: Fired when ICY metadata detects a new song title (sanitized via radio.SanitizeTrackTitle).
    • TrackIdentifiedMsg: Fired when acoustic fingerprinting identifies track via AcoustID/MusicBrainz.
    • RadioBrowserResultsMsg: Fired when online station search returns results.
    • tea.WindowSizeMsg: Terminal resize events to dynamically compute layout dimensions.
    • tea.SetWindowTitle: Dispatches OSC 2 escape sequences to update native terminal window/tab titles dynamically.
  3. View (pkg/ui/view.go)
    Orchestrates sub-component renderers and applies active theme colors to construct the terminal frame.


📦 Package Hierarchy & Responsibilities

halpradio/
├── main.go               # Entry point; embeds stations.yaml & invokes app.Run()
├── stations.yaml         # Bundled station catalog
├── 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
    ├── plugin/           # Wazero Wasm sandboxing engine, capability permissions, host API, registry client
    ├── radio/            # Station catalog store, YAML parser, RadioBrowser client & metadata sanitizer
    ├── 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, LyricsDrawer, AlbumArt)
    └── util/             # OS configuration directory resolution & clipboard utilities

Module Breakdown:

Package Key Types / Files Responsibilities
pkg/app 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 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 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 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 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 Client, Result, LRUCache Captures 5s audio buffers, computes Chromaprint subfingerprints, queries AcoustID & MusicBrainz APIs with LRU caching.
pkg/plugin Manager, Sandbox, Manifest, RegistryClient Executes sandboxed WebAssembly plugins via Wazero with capability checks (network, storage, events). Fetches and verifies official registry packages.
pkg/radio Store, Station, Sanitizer, History Manages bundled, local, and favorite stations. Interfaces with RadioBrowser HTTP API. Cleans dirty metadata and maintains track history.
pkg/theme Theme, GetTheme() Defines 6 color palettes (Tokyo Night, Catppuccin, Synthwave, Nord, Gruvbox, Dracula).
pkg/timer Timer, Event, DispatchEvent() Powers Pomodoro focus interval state machine, sleep timer countdown with volume fade-out, and cross-platform desktop notifications.
pkg/ui Model, KeyMap, Update() Coordinates global navigation state, search filtering, modal popups, and keybindings.
pkg/ui/components Header, StationList, PlayerBar, Visualizer, Modals Render pure, reusable Lipgloss UI components.
pkg/util GetConfigDir(), CopyToClipboard() Provides platform-agnostic file paths for ~/.config/halpradio/ and clipboard integration.

⚡ Data Flow & Concurrency

  1. Audio Playback Subprocess / Goroutine:
    Audio playback runs asynchronously in a separate goroutine managed by player.Manager. This prevents audio streaming or network delays from blocking the Bubble Tea UI event loop.

  2. ICY Metadata Extraction & Sanitization:
    When a station starts playing, player.Manager launches an http.Client request with header Icy-MetaData: 1. As metadata frames arrive, the thread extracts the StreamTitle, passes it through radio.SanitizeTrackTitle to remove ads and station promo noise, and dispatches TrackUpdatedMsg to the Bubble Tea program thread (program.Send()).

  3. Asynchronous Acoustic Stream Fingerprinting:
    Triggered on demand (I) or via --auto-identify when a stream lacks ICY metadata. An asynchronous tea.Cmd captures 5 seconds of stream audio, calculates Chromaprint subfingerprints, queries AcoustID/MusicBrainz, and dispatches TrackIdentifiedMsg to update the player bar, song history, and clipboard without stalling playback.

  4. Asynchronous Plugin Event Bus:
    When tracks or playback states change, plugin.Manager dispatches lifecycle payloads to running WebAssembly sandboxes in parallel background goroutines with timeout limits. Slow or misbehaving plugins can never stall the UI or audio loop.

  5. Local Persistence:
    Favorites, custom user stations, track history, and plugin states are saved to disk under ~/.config/halpradio/ in JSON/YAML format using non-blocking file operations.

  6. Terminal Viewport & Tab Compatibility:
    To prevent vertical scrolling and title clipping across diverse terminal emulators (Ghostty, WezTerm, Kitty, iTerm2, Alacritty, Tmux, Apple Terminal, Windows Terminal):

    • Total rendered height is strictly bounded (lipgloss.Height(view) <= terminal_height - 1).
    • Component inner dimensions explicitly account for Lipgloss borders and padding (innerHeight := height - 2).
    • Column allocation is dynamically responsive, dropping secondary columns (Bitrate/Codec/Flag) when width is constrained.
    • Dynamic terminal window titles are emitted via tea.SetWindowTitle() (OSC 2 sequence), syncing active tab and track status with the host terminal's native tab bar.