261 lines
12 KiB
Markdown
261 lines
12 KiB
Markdown
# External Integrations
|
||
|
||
**Analysis Date:** 2026-02-26
|
||
|
||
## Wails Runtime Bridge (Go ↔ TypeScript)
|
||
|
||
**Primary Communication Mechanism: Events**
|
||
|
||
The Wails runtime provides a bidirectional event bus between Go and TypeScript. Event names are defined as string constants that must match exactly between both sides:
|
||
|
||
- Go: `backend/events/events.go` - Centralized event name constants
|
||
- TypeScript: `frontend/src/events.ts` - Mirrored constants
|
||
|
||
**Event Categories:**
|
||
|
||
| Category | Direction | Events |
|
||
|---|---|---|
|
||
| Playback | Backend → Frontend | `PlaybackStateChanged`, `PlaybackFinished`, `TrackChanged`, `SeekFailed`, `VolumeChanged` |
|
||
| Queue | Backend → Frontend | `QueueChanged`, `QueueIndexChanged`, `QueueModeChanged`, `QueueTracksModified` |
|
||
| Config | Backend → Frontend | `LibraryConfigChanged`, `ThemeConfigChanged`, `TrackListConfigChanged`, `FavoritesConfigChanged` |
|
||
| Playlist | Backend → Frontend | `PlaylistCreated`, `PlaylistDeleted`, `PlaylistRenamed`, `PlaylistTracksChanged`, `PlaylistsRestored`, `DefaultPlaylistChanged` |
|
||
| Library | Backend → Frontend | `LibraryScanStarted`, `LibraryScanComplete` |
|
||
|
||
**Go event emission pattern:**
|
||
```go
|
||
runtime.EventsEmit(p.ctx, events.TrackChanged, trackInfo)
|
||
runtime.EventsOn(l.ctx, events.LibraryConfigChanged, func(data ...any) { ... })
|
||
```
|
||
|
||
**TypeScript event subscription pattern:**
|
||
```typescript
|
||
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo | null) => { ... });
|
||
```
|
||
|
||
**Wails Bindings (Direct Function Calls):**
|
||
|
||
Go structs listed in `FEBindings` in `backend/app.go` are automatically exposed as callable functions from TypeScript. Auto-generated binding stubs live in `frontend/wailsjs/go/` (do not edit).
|
||
|
||
Bound services:
|
||
- `backend/frontendutil/frontendutil.go` → `@go/frontendutil/FrontendUtil` - Directory/file picker dialogs
|
||
- `backend/config/config.go` → `@go/config/Config` - Get/set all configuration
|
||
- `backend/library/library.go` → `@go/library/Library` - Library scanning and queries
|
||
- `backend/playlist/playlist.go` → `@go/playlist/Service` - Playlist CRUD
|
||
- `backend/queue/queue.go` → `@go/queue/Queue` - Queue management
|
||
- `backend/player/player.go` → `@go/player/Player` - Playback control (play, pause, seek, volume, load)
|
||
|
||
**State Synchronization Pattern:**
|
||
|
||
The backend is the source of truth. The frontend requests initial state after its stores are ready:
|
||
```typescript
|
||
// frontend/index.ts (after all stores import and register listeners)
|
||
void Player.EmitCurrentState();
|
||
void Queue.EmitCurrentState();
|
||
```
|
||
|
||
Backend responds by emitting the full current state via events, which the stores receive and cache.
|
||
|
||
## Data Storage
|
||
|
||
**Database: SQLite**
|
||
- Driver: `modernc.org/sqlite` v1.45.0 (pure-Go, no CGo)
|
||
- DB file: `~/.local/share/yellowjacket/yj.db` (Linux)
|
||
- Connection: `backend/database/database.go`
|
||
- Pragmas: WAL journal mode, `busy_timeout=5000`, `foreign_keys=ON`
|
||
- Constraint: `SetMaxOpenConns(1)` (single writer)
|
||
- Code generation: sqlc (`backend/database/sqlc.yaml`)
|
||
- Schemas: `backend/database/sql/schemas/*.sql` (30 schema files)
|
||
- Queries: `backend/database/sql/queries/*.sql` (15 query files)
|
||
- Generated output: `backend/database/sql/sqlcgen/` (DO NOT EDIT)
|
||
- Schema migration: Custom migration system using `PRAGMA user_version` (`backend/database/database.go`, `runMigrations()`)
|
||
- Migration 1: Audio file property columns (sample_rate, bit_depth, channels, bitrate, file_size)
|
||
- Migration 2: Basename column, FTS5 search index
|
||
|
||
**Database Schema (key tables):**
|
||
|
||
| Table | Purpose |
|
||
|---|---|
|
||
| `audio_files` | Tracks with file paths, metadata references, audio properties |
|
||
| `recordings` | Track metadata (title, track number, year, genre, etc.) |
|
||
| `artists` | Artist entities |
|
||
| `artist_credit` | Artist credit display names |
|
||
| `artist_credit_artist` | M:N link between artists and credits |
|
||
| `release_groups` | Albums |
|
||
| `release_group_recordings` | M:N link between albums and recordings |
|
||
| `cover_art` | Cover art file references |
|
||
| `genres` | Genre entities |
|
||
| `genre_recordings` | M:N link between genres and recordings |
|
||
| `playlists` / `playlist_tracks` | User playlists |
|
||
| `queue` / `queue_tracks` | Playback queue with persistence |
|
||
| `player_state` | Persisted player state (volume, last track, position) |
|
||
| `file_types` | Supported audio file type registry |
|
||
| `search_index` | FTS5 full-text search index (file_path, title, artist, album) |
|
||
|
||
**File Storage:**
|
||
- Cover art cache: `~/.local/share/yellowjacket/covers/` (Linux)
|
||
- Managed by `backend/coverart/coverart.go` and `backend/library/coverart.go`
|
||
- Size variants: original, `_sm` (small), `_md` (medium), `_lg` (large)
|
||
- Served via custom asset handler at `/covers/` prefix
|
||
- Config file: `~/.config/yellowjacket/config.toml` (Linux)
|
||
- Managed by `backend/config/config.go`
|
||
- Format: TOML via `github.com/BurntSushi/toml`
|
||
|
||
**Caching:**
|
||
- In-memory entity cache during library scans (`entityCache` in `backend/library/library.go`) - caches artist credits, artists, release groups, cover art, genres to avoid redundant DB upserts
|
||
- No external caching service
|
||
|
||
## Audio Playback
|
||
|
||
**Library: `github.com/gopxl/beep/v2` v2.1.1**
|
||
|
||
Core audio engine providing decode → resample → control → volume → speaker pipeline.
|
||
|
||
- Decoder: `backend/metadata/decoder.go` - Routes by file extension to beep decoders
|
||
- Player: `backend/player/player.go` - Manages streamer chain and playback state
|
||
- Speaker: Initialized at 44100 Hz sample rate, 100ms buffer (`time.Second/10`)
|
||
|
||
**Supported Formats:**
|
||
| Format | Decoder | Extension |
|
||
|---|---|---|
|
||
| MP3 | `github.com/gopxl/beep/v2/mp3` (via `github.com/hajimehoshi/go-mp3`) | `.mp3` |
|
||
| FLAC | `github.com/gopxl/beep/v2/flac` (via `github.com/mewkiz/flac`) | `.flac` |
|
||
| Ogg Vorbis | `github.com/gopxl/beep/v2/vorbis` (via `github.com/jfreymuth/oggvorbis`) | `.ogg` |
|
||
| WAV | `github.com/gopxl/beep/v2/wav` | `.wav` |
|
||
|
||
**Audio Pipeline (per track):**
|
||
1. File opened → decoded to `beep.StreamSeekCloser`
|
||
2. Resampled from source sample rate to speaker rate (44100 Hz, quality=4)
|
||
3. Wrapped in `beep.Ctrl` for play/pause control
|
||
4. Wrapped in `effects.Volume` for volume control (base=2, range -5 to 0 internal)
|
||
5. Registered with `speaker.Play()` with a `beep.Callback` for end-of-track notification
|
||
|
||
**Speaker hardware** uses `github.com/ebitengine/oto/v3` (indirect dependency via beep) for cross-platform audio output.
|
||
|
||
**Volume System:**
|
||
- User-facing: 0–100 integer scale (`player.UserVolume`)
|
||
- Internal: -5.0 to 0.0 float scale (`player.Volume`)
|
||
- Conversion: `backend/player/volume.go`
|
||
|
||
## Metadata Extraction
|
||
|
||
**Library: `github.com/dhowden/tag`**
|
||
|
||
- Extracts ID3v2, Vorbis Comment, and FLAC tags
|
||
- Implementation: `backend/metadata/tags.go` (`ExtractTags`, `ExtractTagsFromReader`)
|
||
- Extracted fields: title, artist, album, album artist, composer, genre, year, track/disc numbers, lyrics, comment, embedded cover art
|
||
|
||
**Custom Duration Parsers:**
|
||
- MP3: `backend/metadata/mp3duration.go` - Custom header parser for accurate duration (handles multiple ID3v2 tags that inflate `go-mp3`'s `Len()`)
|
||
- FLAC: `backend/metadata/flacduration.go` - Custom FLAC STREAMINFO header parser
|
||
- General: `backend/metadata/duration.go` - Fallback using beep decoder for WAV/OGG
|
||
|
||
**Combined Extraction:**
|
||
- `backend/metadata/metadata.go` → `ExtractAllMetadata()` - Single-pass extraction of tags, duration, and audio properties (sample rate, bit depth, channels, bitrate, file size)
|
||
|
||
## System Integrations
|
||
|
||
### MPRIS2 Media Controls (Linux)
|
||
|
||
- Implementation: `backend/mediacontrols/mpris_linux.go` (`//go:build linux`)
|
||
- D-Bus library: `github.com/godbus/dbus/v5`
|
||
- Bus name: `org.mpris.MediaPlayer2.yellowjacket`
|
||
- Object path: `/org/mpris/MediaPlayer2`
|
||
- Interfaces: `org.mpris.MediaPlayer2` (root), `org.mpris.MediaPlayer2.Player`
|
||
- Capabilities: Play, Pause, PlayPause, Stop, Next, Previous, Seek, SetPosition, Volume, Metadata push
|
||
- Non-Linux: No-op stub (`backend/mediacontrols/stub.go`, `//go:build !linux`)
|
||
|
||
**Architecture:** All D-Bus property updates are dispatched via a buffered channel (`updateChanSize = 64`) to a dedicated goroutine, preventing deadlocks between the player mutex and godbus property mutex.
|
||
|
||
### File System
|
||
|
||
- Library scanning: `backend/library/library.go` - Recursive `fs.WalkDir` with concurrent worker pool (`errgroup`)
|
||
- Disk type detection: `backend/system/disktype_linux.go` / `backend/system/disktype_other.go` - Detects HDD vs SSD for adaptive scan concurrency
|
||
- User data directories: `backend/system/userdata.go` - OS-specific paths for config and data
|
||
- Native dialogs: `backend/frontendutil/frontendutil.go` - Directory picker, file picker (for M3U import)
|
||
|
||
### Playlist Import/Export
|
||
|
||
- M3U/M3U8 parsing: `backend/playlist/m3u.go`
|
||
- Playlist matching: `backend/playlist/match.go` - Fuzzy matching of playlist entries to library tracks
|
||
- Favorites system: `backend/playlist/favorites.go` - Special playlist designated as favorites
|
||
|
||
### Cover Art System
|
||
|
||
- Extraction: Embedded art from audio file tags (`backend/library/coverart.go`)
|
||
- Storage: Hash-based filenames in `~/.local/share/yellowjacket/covers/`
|
||
- Size variants: Small (100px), Medium (200px), Large (400px) - generated via `golang.org/x/image`
|
||
- Serving: Custom HTTP handler at `/covers/` prefix (`backend/coverart/handler.go`)
|
||
- URL resolution: `backend/coverart/coverart.go` → `ResolveURLs()` converts filesystem paths to URL paths
|
||
|
||
### Custom Asset Server
|
||
|
||
- Implementation: `backend/assets/handler.go`
|
||
- Serves embedded frontend dist files via Wails asset server
|
||
- Supports custom route registration (used by cover art handler)
|
||
- Middleware pattern captures Wails' default handler for fallback
|
||
|
||
## Frontend Architecture
|
||
|
||
### Entry Points
|
||
|
||
- Main app: `frontend/index.html` → `frontend/index.ts`
|
||
- View routing: DOM-based navigation via `navigate` CustomEvent in `frontend/index.ts`
|
||
- Views: tracks, albums, playlists, artists, genres, libraries, settings, artist-details, genre-details
|
||
|
||
### State Management
|
||
|
||
Singleton stores in `frontend/src/store/`:
|
||
- `player-store.ts` - Playback state, current track, volume
|
||
- `queue-store.ts` - Queue tracks, current index, play mode
|
||
- `library-store.ts` - Library track listing
|
||
- `playlist-store.ts` - Playlist data
|
||
- `favorites-store.ts` - Favorites state
|
||
- `theme-store.ts` - Theme accent color and background shade
|
||
- `search-store.ts` - Search query and results
|
||
- `tracklist-store.ts` - Track list column configuration
|
||
|
||
Each store subscribes to Wails events and delegates actions to backend via Wails bindings.
|
||
|
||
### ReactiveController Pattern
|
||
|
||
Controllers in `frontend/src/store/controllers/` connect Lit components to stores:
|
||
- `player-controller.ts`, `queue-controller.ts`, `library-controller.ts`, `playlist-controller.ts`, `favorites-controller.ts`, `theme-controller.ts`, `search-controller.ts`, `tracklist-controller.ts`
|
||
- Subscribe in `hostConnected()`, unsubscribe in `hostDisconnected()`
|
||
|
||
## Profiling & Observability
|
||
|
||
**Development Only (eliminated in production builds):**
|
||
- pprof HTTP server: `localhost:6060` (`backend/profiling/profiling.go`, `//go:build dev`)
|
||
- Endpoints: `/debug/pprof/`, `/debug/trace`
|
||
- Block and mutex profiling enabled
|
||
- Custom `TimeOp()` function for operation timing
|
||
|
||
**Logging:**
|
||
- Framework: `log/slog` (structured, key-value pairs)
|
||
- Dev handler: `github.com/golang-cz/devslog` (pretty-printed to stdout)
|
||
- Wails logger bridge: `backend/logging/logging.go` (routes Wails logs through slog)
|
||
- Pattern: Logger injected via constructors, scoped with `logger.WithGroup("component")`
|
||
|
||
## External APIs & Services
|
||
|
||
**None.** YellowJacket is a fully local, offline application. There are no external API calls, cloud services, analytics, telemetry, or network requests. All data lives on the local filesystem.
|
||
|
||
## CI/CD & Deployment
|
||
|
||
**CI Pipeline:** Not detected in the repository (no `.github/workflows/`, `.gitlab-ci.yml`, etc.)
|
||
|
||
**Git Hooks (lefthook):**
|
||
- `lefthook.yml` - Pre-commit: go vet, golangci-lint, codegen check, frontend typecheck
|
||
- Pre-push: protect main branch, go test, go mod verify
|
||
|
||
**Distribution:** Binary builds via `make build-prod` (obfuscated + UPX compressed)
|
||
|
||
## Webhooks & Callbacks
|
||
|
||
**Incoming:** None
|
||
**Outgoing:** None
|
||
|
||
---
|
||
|
||
*Integration audit: 2026-02-26*
|