Skip to content

Commit 37046ba

Browse files
committed
Merge branch 'main' of ssh://github.com/hilli/go-kef-w2
2 parents fc67d00 + 19cd10d commit 37046ba

19 files changed

Lines changed: 1628 additions & 549 deletions

CHANGELOG.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.6] - 2026-02-06
11+
12+
### Added
13+
14+
- **Stop command**: New `kefw2 stop` command to stop playback entirely
15+
- Unlike pause, stop ends the current stream completely
16+
- Useful for radio and live streams where pause is not meaningful
17+
- **Library: Stop method**: New `Stop(ctx)` method for programmatic stream termination
18+
- **Browse container configuration**: Set a default starting folder for UPnP browsing
19+
- `config upnp container browse <path>` - Set the starting container for browsing
20+
- Skips parent containers and other servers for a cleaner navigation experience
21+
- **Browse cache**: New browse cache system for faster navigation and tab completion
22+
- Caches container listings with configurable TTLs per service type
23+
- Automatic cache persistence and cleanup
24+
- **Cache search**: Search cached entries for quick offline lookups
25+
- **Library: Track indexing moved to kefw2 package**: Track index functionality is now part of the library
26+
- `kefw2.LoadTrackIndex()`, `kefw2.SaveTrackIndex()`, `kefw2.BuildTrackIndex()`
27+
- `kefw2.SearchTracks()`, `kefw2.TrackIndexPath()`, `kefw2.IsTrackIndexFresh()`
28+
- `kefw2.FindContainerByPath()`, `kefw2.ListContainersAtPath()`
29+
30+
### Changed
31+
32+
- **Reorganized UPnP container config**: Moved from `config upnp index container` to `config upnp container index`
33+
- `config upnp container browse` - Configure starting folder for browsing
34+
- `config upnp container index` - Configure folder to index for search
35+
- **Improved queue playback**: Fixed queue track playback to properly handle metadata and resources
36+
- **Enhanced UPnP track metadata handling**: Preserve original serviceID and support Airable-specific metadata fields
37+
38+
### Fixed
39+
40+
- **Speaker discovery goroutine leak**: Fixed dnssd goroutine leak by using context.WithCancel instead of context.WithTimeout
41+
- **Player event duration fallback**: Duration now correctly falls back to MediaData.Resources or ActiveResource when Status.Duration is zero
42+
- **Podcast playback**: Improved handling of Airable podcast authentication by playing through parent containers
43+
- **Queue index playback**: Queue items now fetch track details when not provided
44+
1045
## [0.2.5] - 2026-02-05
1146

1247
### Added
@@ -382,7 +417,8 @@ Implemented by: `Source`, `SpeakerStatus`, `CableMode`
382417

383418
7. **Update player ID field access**: If you access `playId.SystemMemberId`, change it to `playId.SystemMemberID`.
384419

385-
[Unreleased]: https://github.com/hilli/go-kef-w2/compare/v0.2.5...HEAD
420+
[Unreleased]: https://github.com/hilli/go-kef-w2/compare/v0.2.6...HEAD
421+
[0.2.6]: https://github.com/hilli/go-kef-w2/compare/v0.2.5...v0.2.6
386422
[0.2.5]: https://github.com/hilli/go-kef-w2/compare/v0.2.4...v0.2.5
387423
[0.2.4]: https://github.com/hilli/go-kef-w2/compare/v0.2.3...v0.2.4
388424
[0.2.3]: https://github.com/hilli/go-kef-w2/compare/v0.2.2...v0.2.3

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ kefw2 play
123123
kefw2 pause
124124
```
125125

126+
Stop playback entirely (useful for radio/live streams)
127+
128+
```shell
129+
kefw2 stop
130+
```
131+
126132
Seek to a specific position in the current track or podcast
127133

128134
```shell
@@ -246,7 +252,10 @@ scanning duplicate views (By Genre, By Album, etc. contain the same tracks):
246252
247253
```shell
248254
# Set the container path to index (with tab completion)
249-
kefw2 config upnp index container "Music/Hilli's Music/By Folder"
255+
kefw2 config upnp container index "Music/Hilli's Music/By Folder"
256+
257+
# Set a default starting folder for browsing
258+
kefw2 config upnp container browse "Music/Hilli's Music"
250259
251260
# Show current index status
252261
kefw2 upnp index
@@ -258,7 +267,7 @@ kefw2 upnp index --rebuild
258267
kefw2 upnp index --rebuild --container "Music/My Library/By Album"
259268
260269
# Clear the container setting (index entire server)
261-
kefw2 config upnp index container ""
270+
kefw2 config upnp container index ""
262271
```
263272
264273
The container path uses `/` as separator and supports tab completion at each level.

cmd/kefw2/cmd/cache.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"sync"
3030
"time"
3131

32+
"github.com/hilli/go-kef-w2/kefw2"
3233
"github.com/spf13/cobra"
3334
"github.com/spf13/viper"
3435
)
@@ -406,7 +407,7 @@ var cacheStatusCmd = &cobra.Command{
406407

407408
// Show UPnP track index status
408409
headerPrinter.Println("\nUPnP Track Index:")
409-
index, err := LoadTrackIndex()
410+
index, err := kefw2.LoadTrackIndex()
410411
if err != nil || index == nil {
411412
contentPrinter.Println(" No index found")
412413
contentPrinter.Println(" Run 'kefw2 upnp index --rebuild' to create one")
@@ -417,7 +418,7 @@ var cacheStatusCmd = &cobra.Command{
417418
}
418419
contentPrinter.Printf(" Tracks: %d\n", index.TrackCount)
419420
contentPrinter.Printf(" Age: %v\n", time.Since(index.IndexedAt).Round(time.Second))
420-
contentPrinter.Printf(" Location: %s\n", getTrackIndexPath())
421+
contentPrinter.Printf(" Location: %s\n", kefw2.TrackIndexPath())
421422
}
422423
},
423424
}

cmd/kefw2/cmd/config_upnp.go

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -153,39 +153,110 @@ func UPnPServerCompletion(_ *cobra.Command, args []string, toComplete string) ([
153153
}
154154

155155
// upnpIndexConfigCmd is the parent for index config subcommands.
156-
var upnpIndexConfigCmd = &cobra.Command{
157-
Use: "index",
158-
Short: "Configure UPnP search index settings",
159-
Long: `Configure settings for the UPnP search index, including the container path to index.`,
156+
var upnpContainerConfigCmd = &cobra.Command{
157+
Use: "container",
158+
Short: "Configure UPnP container paths",
159+
Long: `Configure container paths for browsing and indexing.`,
160160
Run: func(cmd *cobra.Command, _ []string) {
161161
_ = cmd.Help()
162162
},
163163
}
164164

165-
// upnpIndexContainerCmd shows or sets the index container path.
166-
var upnpIndexContainerCmd = &cobra.Command{
167-
Use: "container [path]",
165+
// upnpContainerBrowseCmd shows or sets the browse container path.
166+
var upnpContainerBrowseCmd = &cobra.Command{
167+
Use: "browse [path]",
168+
Short: "Show or set the container path for browsing",
169+
Long: `Show the current container path for browsing, or set a new one.
170+
171+
The browse container determines the starting point when browsing your UPnP library.
172+
When set, you won't see parent containers or other servers - the browse container
173+
becomes your "root" for navigation.
174+
175+
Use "/" as separator for nested paths.
176+
177+
Without arguments, displays the current setting.
178+
With a path, sets that as the starting container for browsing.
179+
180+
Examples:
181+
kefw2 config upnp container browse # Show current
182+
kefw2 config upnp container browse "Music" # Start from Music folder
183+
kefw2 config upnp container browse "Music/Hilli's Music" # Start from specific folder
184+
kefw2 config upnp container browse "" # Clear (show all servers)`,
185+
Run: func(_ *cobra.Command, args []string) {
186+
if len(args) == 0 {
187+
// Show current setting
188+
containerPath := viper.GetString("upnp.browse_container")
189+
if containerPath == "" {
190+
contentPrinter.Println("No browse container configured (will show all servers).")
191+
contentPrinter.Println("Use 'kefw2 config upnp container browse <path>' to set one.")
192+
return
193+
}
194+
headerPrinter.Print("Browse container: ")
195+
contentPrinter.Println(containerPath)
196+
return
197+
}
198+
199+
// Set new container path
200+
containerPath := args[0]
201+
202+
// If a path is provided, validate it exists
203+
if containerPath != "" {
204+
serverPath := viper.GetString("upnp.default_server_path")
205+
if serverPath == "" {
206+
exitWithError("No default UPnP server configured. Set one first with: kefw2 config upnp server default <name>")
207+
}
208+
209+
client := kefw2.NewAirableClient(currentSpeaker)
210+
_, resolvedName, err := kefw2.FindContainerByPath(client, serverPath, containerPath)
211+
if err != nil {
212+
exitWithError("Invalid container path: %v", err)
213+
}
214+
// Use the resolved path (with proper casing)
215+
containerPath = resolvedName
216+
}
217+
218+
// Save to config
219+
viper.Set("upnp.browse_container", containerPath)
220+
err := viper.WriteConfig()
221+
exitOnError(err, "Error saving config")
222+
223+
if containerPath == "" {
224+
taskConpletedPrinter.Println("Browse container cleared (will show all servers)")
225+
} else {
226+
taskConpletedPrinter.Print("Browse container set: ")
227+
contentPrinter.Println(containerPath)
228+
}
229+
},
230+
ValidArgsFunction: UPnPContainerCompletion,
231+
}
232+
233+
// upnpContainerIndexCmd shows or sets the index container path.
234+
var upnpContainerIndexCmd = &cobra.Command{
235+
Use: "index [path]",
168236
Short: "Show or set the container path for indexing",
169237
Long: `Show the current container path for indexing, or set a new one.
170238
171239
The container path determines which folder to start indexing from.
172240
Use "/" as separator for nested paths.
173241
242+
TIP: For best results, use a "By Folder" path (e.g., "Music/Hilli's Music/By Folder").
243+
This indexes your actual folder structure without any reorganization by the media server.
244+
174245
Without arguments, displays the current setting.
175246
With a path, sets that as the default container to index.
176247
177248
Examples:
178-
kefw2 config upnp index container # Show current
179-
kefw2 config upnp index container "Music" # Index Music folder
180-
kefw2 config upnp index container "Music/Hilli's Music/By Folder" # Index specific folder
181-
kefw2 config upnp index container "" # Clear (index entire server)`,
249+
kefw2 config upnp container index # Show current
250+
kefw2 config upnp container index "Music" # Index Music folder
251+
kefw2 config upnp container index "Music/Hilli's Music/By Folder" # Index by folder (recommended)
252+
kefw2 config upnp container index "" # Clear (index entire server)`,
182253
Run: func(_ *cobra.Command, args []string) {
183254
if len(args) == 0 {
184255
// Show current setting
185256
containerPath := viper.GetString("upnp.index_container")
186257
if containerPath == "" {
187258
contentPrinter.Println("No index container configured (will index entire server).")
188-
contentPrinter.Println("Use 'kefw2 config upnp index container <path>' to set one.")
259+
contentPrinter.Println("Use 'kefw2 config upnp container index <path>' to set one.")
189260
return
190261
}
191262
headerPrinter.Print("Index container: ")
@@ -204,12 +275,12 @@ Examples:
204275
}
205276

206277
client := kefw2.NewAirableClient(currentSpeaker)
207-
_, resolvedPath, err := findContainerByPath(client, serverPath, containerPath)
278+
_, resolvedName, err := kefw2.FindContainerByPath(client, serverPath, containerPath)
208279
if err != nil {
209280
exitWithError("Invalid container path: %v", err)
210281
}
211282
// Use the resolved path (with proper casing)
212-
containerPath = resolvedPath
283+
containerPath = resolvedName
213284
}
214285

215286
// Save to config
@@ -256,7 +327,7 @@ func UPnPContainerCompletion(_ *cobra.Command, args []string, toComplete string)
256327
}
257328

258329
// Get containers at the parent path
259-
containers, err := listContainersAtPath(client, serverPath, parentPath)
330+
containers, err := kefw2.ListContainersAtPath(client, serverPath, parentPath)
260331
if err != nil {
261332
return nil, cobra.ShellCompDirectiveNoFileComp
262333
}
@@ -285,6 +356,7 @@ func init() {
285356
upnpConfigCmd.AddCommand(upnpServerConfigCmd)
286357
upnpServerConfigCmd.AddCommand(upnpServerDefaultCmd)
287358
upnpServerConfigCmd.AddCommand(upnpServerListCmd)
288-
upnpConfigCmd.AddCommand(upnpIndexConfigCmd)
289-
upnpIndexConfigCmd.AddCommand(upnpIndexContainerCmd)
359+
upnpConfigCmd.AddCommand(upnpContainerConfigCmd)
360+
upnpContainerConfigCmd.AddCommand(upnpContainerBrowseCmd)
361+
upnpContainerConfigCmd.AddCommand(upnpContainerIndexCmd)
290362
}

cmd/kefw2/cmd/stop.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright © 2023-2026 Jens Hilligsøe
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in
12+
all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
THE SOFTWARE.
21+
*/
22+
package cmd
23+
24+
import (
25+
"github.com/spf13/cobra"
26+
)
27+
28+
// stopCmd stops playback entirely (useful for radio/live streams where pause is not meaningful).
29+
var stopCmd = &cobra.Command{
30+
Use: "stop",
31+
Short: "Stop playback when on WiFi/BT source",
32+
Long: `Stop playback entirely. Unlike pause, this ends the current stream and is particularly useful for radio and live streams.`,
33+
Args: cobra.MaximumNArgs(0),
34+
Run: func(cmd *cobra.Command, _ []string) {
35+
ctx := cmd.Context()
36+
canControlPlayback, err := currentSpeaker.CanControlPlayback(ctx)
37+
exitOnError(err, "Can't stop speaker")
38+
if !canControlPlayback {
39+
headerPrinter.Println("Can't stop speaker: Not on WiFi/BT source.")
40+
return
41+
}
42+
err = currentSpeaker.Stop(ctx)
43+
exitOnError(err, "Can't stop playback")
44+
},
45+
}
46+
47+
func init() {
48+
rootCmd.AddCommand(stopCmd)
49+
}

0 commit comments

Comments
 (0)