All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.7 - 2026-02-11
- Standby wake on play:
PlayOrResumeFromQueuenow automatically switches to WiFi and waits for the speaker to wake when called from standby, then starts playback from the queue - Library:
WokeFromStandbyfield onPlayResult: Callers can check whether a standby wake occurred during playback start - Library:
AlbumsForArtist()helper: New function andArtistAlbumtype for extracting unique albums from artist search results
playcommand wakes from standby: The CLI play command now wakes the speaker from standby instead of refusing; still refuses on non-streamable physical sources (optical, coaxial, etc.)- Goreleaser: Homebrew cask: Switched from Homebrew formula to Homebrew cask with shell completion installation and macOS quarantine removal
- Renamed
mintominsin seek: Avoids shadowing the Go 1.21+minbuiltin
- PlayerTrackRoles documentation: Corrected
PathandIDfield comments — these are internal item IDs, not display indices - Airable redirect handling: Radio and podcast menu endpoints now properly follow redirects and return rows from the redirected path
- Import ordering: Fixed import grouping in
cache.go
0.2.6 - 2026-02-06
- Stop command: New
kefw2 stopcommand to stop playback entirely- Unlike pause, stop ends the current stream completely
- Useful for radio and live streams where pause is not meaningful
- Library: Stop method: New
Stop(ctx)method for programmatic stream termination - Browse container configuration: Set a default starting folder for UPnP browsing
config upnp container browse <path>- Set the starting container for browsing- Skips parent containers and other servers for a cleaner navigation experience
- Browse cache: New browse cache system for faster navigation and tab completion
- Caches container listings with configurable TTLs per service type
- Automatic cache persistence and cleanup
- Cache search: Search cached entries for quick offline lookups
- Library: Track indexing moved to kefw2 package: Track index functionality is now part of the library
kefw2.LoadTrackIndex(),kefw2.SaveTrackIndex(),kefw2.BuildTrackIndex()kefw2.SearchTracks(),kefw2.TrackIndexPath(),kefw2.IsTrackIndexFresh()kefw2.FindContainerByPath(),kefw2.ListContainersAtPath()
- Reorganized UPnP container config: Moved from
config upnp index containertoconfig upnp container indexconfig upnp container browse- Configure starting folder for browsingconfig upnp container index- Configure folder to index for search
- Improved queue playback: Fixed queue track playback to properly handle metadata and resources
- Enhanced UPnP track metadata handling: Preserve original serviceID and support Airable-specific metadata fields
- Speaker discovery goroutine leak: Fixed dnssd goroutine leak by using context.WithCancel instead of context.WithTimeout
- Player event duration fallback: Duration now correctly falls back to MediaData.Resources or ActiveResource when Status.Duration is zero
- Podcast playback: Improved handling of Airable podcast authentication by playing through parent containers
- Queue index playback: Queue items now fetch track details when not provided
0.2.5 - 2026-02-05
- Seek Command: New
kefw2 seek <position>command for jumping to a specific position in the current track- Supports multiple time formats:
hh:mm:ss,mm:ss, or seconds - Examples:
seek 1:30(1 min 30 sec),seek 1:23:45(1 hr 23 min 45 sec),seek 90(90 seconds)
- Supports multiple time formats:
- Library: SeekTo method: New
SeekTo(ctx, positionMS)method for programmatic seeking within tracks
0.2.4 - 2026-02-04
- Improve station selection logic for radio playback
radio browsewith no arguments now shows the interactive picker instead of attempting to auto
0.2.3 - 2025-02-04
- Fixed playback from
upnp searchresults - tracks now play and add to queue correctly- The search index was missing audio file URIs required for playback
- Index version bumped to v2; run
kefw2 upnp index --rebuildafter updating
0.2.2 - 2025-02-03
-
UPnP Library Search: New local search index for instant track searching
upnp search [query]- Search by title, artist, or albumupnp search(no query) - Browse full library with interactive filterupnp index- View index statusupnp index --rebuild- Rebuild the search indexupnp index --container "path"- Index from specific folderconfig upnp index container- Set default container for indexing- Ranked search results with exact matches first
- Multi-word queries work across fields (e.g.,
public enemy uzi)
-
Content Picker Improvements
- Page Up/Page Down navigation for faster scrolling
- Filter now matches on artist/album in addition to title
upnp playnow recursively scans sub-containers (plays all albums under an artist)upnp searchaccepts multiple arguments without quotes
- Fixed
cache statusto show correct cache file and entry count
0.2.1 - 2026-02-02
- Full UPnP pagination support with
BrowseContainerAll()for fetching all items in large directories AddContainerToQueuecallback for recursively adding all tracks from containersGetContainerTracksRecursive()for finding all tracks in nested foldersBrowseUPnPByDisplayPathAll()for full pagination on display paths- Enhanced keyboard shortcuts for queue management
- Interactive content picker now uses
BrowseContainerAll()to fetch all items - Improved recursive track addition from containers
- More accurate item filtering and selection in content picker
- Consistent use of full container browsing across UPnP and podcast features
- Fixed podcast play/search handling
- Fixed queue picker's delete and clear commands
- Fixed UPnP
IsPlayableto treat all containers as navigable (not playable)
- Removed
queue addcommand (use interactive content picker instead)
0.2.0 - 2026-02-01
New kefw2 radio command for streaming internet radio via KEF's Airable integration:
radio play <station>- Play a radio station with full tab completionradio favorites- List and play favorite stationsradio popular,local,trending,hq,new- Browse radio categoriesradio search <query>- Search for stationsradio browse- Interactive TUI browser
New kefw2 podcast command for podcast playback:
podcast play <show/episode>- Play episodes with hierarchical tab completion (Show Name/Episode)podcast favorites- List and play favorite podcastspodcast popular,trending,history- Browse podcast categoriespodcast search <query>- Search for podcastspodcast browse- Interactive TUI browser
New kefw2 upnp command for playing from local network media servers:
upnp browse [path]- Browse server contents with tab completionupnp play <path>- Play media filesconfig upnp server <name>- Set default UPnP server
New kefw2 queue command for managing playback queue:
queue list- Show current queue contentsqueue add <item>- Add items to queuequeue clear- Clear the queuequeue save <name>- Save queue as presetqueue load <name>- Load saved queue presetqueue mode <mode>- Set repeat/shuffle mode
New caching system for faster tab completion with configurable TTL:
config cache- Show all cache settingsconfig cache enable/disable- Toggle cachingconfig cache ttl-default- TTL for new/unknown services (default: 300s)config cache ttl-radio- TTL for radio cache (default: 300s)config cache ttl-podcast- TTL for podcast cache (default: 300s)config cache ttl-upnp- TTL for UPnP cache (default: 60s)cache status- View cache statisticscache clear- Clear cached data
New AirableClient for programmatic access to streaming services:
client := kefw2.NewAirableClient(speaker)
// Radio
favorites, _ := client.GetRadioFavorites(ctx)
client.PlayRadioStation(ctx, stationPath)
// Podcasts
episodes, _ := client.GetPodcastEpisodes(ctx, podcastPath)
client.PlayPodcastEpisode(ctx, episodePath)
// UPnP
servers, _ := client.GetMediaServers(ctx)
client.BrowseContainer(ctx, serverPath)- Hierarchical path completion for radio, podcasts, and UPnP (
Parent/Child/Item) - Colon escaping for zsh compatibility (
:→%3A) - Real-time current value display in completions
- Pagination support for large result sets (
*All()methods)
config cachecommand redesigned: shows all settings when called without arguments- Moved
cache configtoconfig cachefor consistency - Improved error messages for missing speaker configuration
- Tab completion now uses
*All()methods consistently to avoid truncated results - Fixed colon characters breaking tab completion in zsh
0.1.0 - 2026-01-30
-
Context-first API: All speaker methods now require
context.Contextas their first parameter. The*Contextvariant methods have been removed.// Old volume, err := speaker.GetVolume() err = speaker.SetVolume(50) err = speaker.Mute() source, err := speaker.Source() // New ctx := context.Background() volume, err := speaker.GetVolume(ctx) err = speaker.SetVolume(ctx, 50) err = speaker.Mute(ctx) source, err := speaker.Source(ctx)
Methods updated:
GetVolume(ctx),SetVolume(ctx, volume),Mute(ctx),Unmute(ctx),IsMuted(ctx)Source(ctx),SetSource(ctx, source),PowerOff(ctx),SpeakerState(ctx),IsPoweredOn(ctx)PlayPause(ctx),NextTrack(ctx),PreviousTrack(ctx),IsPlaying(ctx)GetMaxVolume(ctx),SetMaxVolume(ctx, volume),SongProgress(ctx),SongProgressMS(ctx)CanControlPlayback(ctx),NetworkOperationMode(ctx),UpdateInfo(ctx)PlayerData(ctx),GetEQProfileV2(ctx)
-
Removed
*Contextvariant methods: Methods likeGetVolumeContext,SetVolumeContext,MuteContext, etc. have been removed. Use the standard methods with context instead. -
NewSpeakernow returns a pointer:NewSpeaker()returns(*KEFSpeaker, error)instead of(KEFSpeaker, error).// Old speaker, err := kefw2.NewSpeaker("192.168.1.100") // New speaker, err := kefw2.NewSpeaker("192.168.1.100") // Returns *KEFSpeaker
-
DiscoverSpeakerssignature changed: Now acceptscontext.Contextandtime.Durationinstead of anintfor timeout seconds.// Old speakers, err := kefw2.DiscoverSpeakers(5) // 5 seconds // New ctx := context.Background() speakers, err := kefw2.DiscoverSpeakers(ctx, 5*time.Second)
A legacy wrapper
DiscoverSpeakersLegacy(int)is available for backward compatibility but is deprecated. -
Renamed
KEFSpeaker.IdtoKEFSpeaker.ID: Following Go naming conventions. -
Renamed
KEFGroupingmembertoKEFGroupingMember: Fixed casing to follow Go naming conventions. -
Renamed
EQProfileV2.ProfileIdtoEQProfileV2.ProfileID: Following Go naming conventions for acronyms. JSON serialization unchanged (profileId). -
Renamed
PlayerPlayID.SystemMemberIdtoPlayerPlayID.SystemMemberID: Following Go naming conventions for acronyms. JSON serialization unchanged (systemMemberId).
NewSpeaker now supports functional options for configuration:
speaker, err := kefw2.NewSpeaker("192.168.1.100",
kefw2.WithTimeout(5*time.Second),
kefw2.WithHTTPClient(customClient),
)All speaker methods now accept context.Context for better control over request cancellation and timeouts:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
volume, err := speaker.GetVolume(ctx)
if err != nil {
// Handle timeout or cancellation
}New sentinel errors for better error handling:
ErrConnectionRefused- Speaker not responding (powered off or unreachable)ErrConnectionTimeout- Request timed outErrHostNotFound- Invalid IP address or hostnameErrEmptyData- Empty response from speakerErrNoValue- No value in API responseErrUnknownType- Unknown value type in responseErrInvalidFormat- Malformed JSON response
New TypeEncoder interface for KEF-specific types, allowing custom types to be used with setTypedValue:
type TypeEncoder interface {
KEFTypeInfo() (typeName string, value string)
}Implemented by: Source, SpeakerStatus, CableMode
- Added package-level documentation with usage examples
- Added godoc comments to all exported types, constants, and functions
- Documented all struct fields
- Added 160+ unit tests covering:
- JSON parsing functions
- HTTP client operations
- Speaker operations (volume, mute, source, power, playback)
- Type encoding and string conversion
- Event types and parsing
- Context cancellation and timeout handling
- Error handling edge cases
- Unified HTTP client: Single shared
*http.Clientper speaker instance - Centralized request handling: All HTTP requests go through
doRequest()method - Replaced
logruswithslog: Using standard library structured logging in the library (CLI still uses logrus) - Improved JSON parsing: New internal functions with safe type assertions:
parseJSONString()parseJSONInt()parseJSONBool()parseJSONValue()
- Better error wrapping: Using
%wverb consistently for error chains - Channel-based discovery:
DiscoverSpeakersuses proper channel synchronization instead oftime.Sleep
JSONStringValue(data []byte, err error)- UseparseJSONString()(internal) or handle errors separatelyJSONIntValue(data []byte, err error)- UseparseJSONInt()(internal)JSONUnmarshalValue(data []byte, err error)- UseparseJSONValue()(internal)DiscoverSpeakersLegacy(timeout int)- UseDiscoverSpeakers(ctx, duration)
- Fixed go vet warning in
cmd/kef-virtual-hub/kef-virtual-hub.go(buffered signal channel) - Fixed inconsistent pointer/value receiver usage on methods
-
Add context to all speaker method calls: All speaker methods now require
context.Contextas the first parameter.// Before volume, err := speaker.GetVolume() err = speaker.SetVolume(50) err = speaker.Mute() // After ctx := context.Background() volume, err := speaker.GetVolume(ctx) err = speaker.SetVolume(ctx, 50) err = speaker.Mute(ctx)
-
Remove
*Contextmethod calls: If you were using methods likeGetVolumeContext, rename them to the standard method name.// Before volume, err := speaker.GetVolumeContext(ctx) // After volume, err := speaker.GetVolume(ctx)
-
Update
NewSpeakercalls: The return type is now a pointer, but if you were already using the result directly, no changes are needed since methods work on pointer receivers. -
Update
DiscoverSpeakerscalls:// Before speakers, err := kefw2.DiscoverSpeakers(5) // After speakers, err := kefw2.DiscoverSpeakers(context.Background(), 5*time.Second)
-
Update field access: If you access
speaker.Id, change it tospeaker.ID. -
Update EQ profile field access: If you access
eqProfile.ProfileId, change it toeqProfile.ProfileID. -
Update player ID field access: If you access
playId.SystemMemberId, change it toplayId.SystemMemberID.