Skip to content

Commit 4c4ae23

Browse files
authored
Fix: Store URI and MimeType in track index for playback (#23)
* Fix: Store URI and MimeType in track index for playback The track index was missing the audio file URI and MIME type, which are required by the KEF speaker to play tracks. This caused 'upnp search' results to fail when playing or adding to queue, while 'upnp browse' worked correctly (as it fetched full track data). Changes: - Add URI and MimeType fields to IndexedTrack struct - Store these fields when building the index - Include them when converting IndexedTrack back to ContentItem - Bump index version to 2 (old indexes are auto-invalidated) - Update CHANGELOG.md for v0.2.3 Users need to run 'kefw2 upnp index --rebuild' once after this update. * Fix linting issues: godot, unused params, gosec, and more - Add periods to all doc comments (godot) - Rename unused function parameters to _ (unused-parameter) - Use stricter file permissions 0750/0600 (gosec) - Convert if-else chains to switch statements (staticcheck) - Use http.NewRequestWithContext (noctx) - Fix ineffective assignments (ineffassign) - Preallocate slices where applicable (prealloc) - Extract repeated strings to constants (goconst) - Use assignment operators x += y (assignOp) - Fix deprecated comment format (deprecatedComment) - Fix spelling: Cancelled -> Canceled (misspell) Reduces linting issues from 64 to 8 (only gocyclo warnings remain, which are acceptable for TUI Update functions). * Use CHANGELOG.md for GitHub release notes Extract version-specific notes from CHANGELOG.md instead of auto-generating from commit messages. The workflow now: - Extracts the matching version section from CHANGELOG.md - Strips the version header (GitHub shows the tag separately) - Falls back to a link if no entry is found
1 parent 194e712 commit 4c4ae23

28 files changed

Lines changed: 466 additions & 370 deletions

.github/workflows/release.yaml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,39 @@ jobs:
2020
with:
2121
go-version: '1.24'
2222
check-latest: true
23+
- name: Extract release notes from CHANGELOG.md
24+
run: |
25+
# Strip 'v' prefix from tag to match CHANGELOG format
26+
VERSION="${GITHUB_REF_NAME#v}"
27+
echo "Extracting release notes for version: $VERSION"
28+
29+
# Extract section for this version (from ## [x.y.z] to next ## [ or EOF)
30+
# Skip the first line (version header) since GitHub shows the tag separately
31+
awk -v ver="$VERSION" '
32+
/^## \[/ {
33+
if (found) exit
34+
if ($0 ~ "\\[" ver "\\]") { found=1; next }
35+
}
36+
found { print }
37+
' CHANGELOG.md > /tmp/release-notes.md
38+
39+
# Remove leading blank lines
40+
sed -i '/./,$!d' /tmp/release-notes.md
41+
42+
# Check if we found anything
43+
if [ ! -s /tmp/release-notes.md ]; then
44+
echo "Warning: No changelog entry found for version $VERSION"
45+
echo "See [CHANGELOG.md](https://github.com/hilli/go-kef-w2/blob/main/CHANGELOG.md) for details." > /tmp/release-notes.md
46+
fi
47+
48+
echo "--- Release notes ---"
49+
cat /tmp/release-notes.md
50+
echo "--- End release notes ---"
2351
- name: Release
2452
uses: goreleaser/goreleaser-action@v6
2553
with:
2654
version: latest
27-
args: release --clean
55+
args: release --clean --release-notes=/tmp/release-notes.md
2856
env:
2957
GITHUB_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
3058
MASTODON_CLIENT_ID: ${{ secrets.MASTODON_CLIENT_ID }}

.goreleaser.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,4 @@ announce:
150150
server: https://mastodon.social
151151

152152
changelog:
153-
sort: asc
154-
filters:
155-
exclude:
156-
- "^docs:"
157-
- "^test:"
153+
disable: true

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [0.2.3] - 2025-02-04
11+
12+
### Fixed
13+
14+
- Fixed playback from `upnp search` results - tracks now play and add to queue correctly
15+
- The search index was missing audio file URIs required for playback
16+
- Index version bumped to v2; run `kefw2 upnp index --rebuild` after updating
17+
1018
## [0.2.2] - 2025-02-03
1119

1220
### Added
@@ -358,7 +366,8 @@ Implemented by: `Source`, `SpeakerStatus`, `CableMode`
358366

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

361-
[Unreleased]: https://github.com/hilli/go-kef-w2/compare/v0.2.2...HEAD
369+
[Unreleased]: https://github.com/hilli/go-kef-w2/compare/v0.2.3...HEAD
370+
[0.2.3]: https://github.com/hilli/go-kef-w2/compare/v0.2.2...v0.2.3
362371
[0.2.2]: https://github.com/hilli/go-kef-w2/compare/v0.2.1...v0.2.2
363372
[0.2.1]: https://github.com/hilli/go-kef-w2/compare/v0.2.0...v0.2.1
364373
[0.2.0]: https://github.com/hilli/go-kef-w2/compare/v0.1.0...v0.2.0

cmd/kefw2/cmd/browser_styles.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ package cmd
2323

2424
import "github.com/charmbracelet/lipgloss"
2525

26-
// ServiceType represents the type of content service (radio, podcast, upnp)
26+
// ServiceType represents the type of content service (radio, podcast, upnp).
2727
type ServiceType string
2828

2929
const (
@@ -32,14 +32,14 @@ const (
3232
ServiceUPnP ServiceType = "upnp"
3333
)
3434

35-
// ServiceColors defines the color scheme for each service type
35+
// ServiceColors defines the color scheme for each service type.
3636
var ServiceColors = map[ServiceType]lipgloss.Color{
3737
ServiceRadio: lipgloss.Color("39"), // Blue for radio
3838
ServicePodcast: lipgloss.Color("207"), // Pink/magenta for podcast
3939
ServiceUPnP: lipgloss.Color("214"), // Orange for UPnP
4040
}
4141

42-
// BrowserStyles holds styled renderers for the content browser TUI
42+
// BrowserStyles holds styled renderers for the content browser TUI.
4343
type BrowserStyles struct {
4444
Title lipgloss.Style
4545
Search lipgloss.Style
@@ -50,7 +50,7 @@ type BrowserStyles struct {
5050
Dimmed lipgloss.Style
5151
}
5252

53-
// NewBrowserStyles creates a new set of browser styles for the given service type
53+
// NewBrowserStyles creates a new set of browser styles for the given service type.
5454
func NewBrowserStyles(service ServiceType) BrowserStyles {
5555
color := ServiceColors[service]
5656
if color == "" {
@@ -87,11 +87,11 @@ func NewBrowserStyles(service ServiceType) BrowserStyles {
8787
}
8888
}
8989

90-
// Common styles used across all services
90+
// Common styles used across all services.
9191
var (
92-
// containerSuffix is shown after container names
92+
// containerSuffix is shown after container names.
9393
containerSuffix = "/"
9494

95-
// maxVisibleItems is the number of items visible in the picker at once
95+
// maxVisibleItems is the number of items visible in the picker at once.
9696
maxVisibleItems = 15
9797
)

cmd/kefw2/cmd/cache.go

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
"github.com/spf13/viper"
3434
)
3535

36-
// CachedItem represents a cached content item for completion
36+
// CachedItem represents a cached content item for completion.
3737
type CachedItem struct {
3838
Title string `json:"title"`
3939
Path string `json:"path"` // Actual API path
@@ -42,24 +42,24 @@ type CachedItem struct {
4242
Description string `json:"description"` // Optional description
4343
}
4444

45-
// CacheEntry represents a cached response for a browse path
45+
// CacheEntry represents a cached response for a browse path.
4646
type CacheEntry struct {
4747
Items []CachedItem `json:"items"`
4848
FetchedAt time.Time `json:"fetched_at"`
4949
}
5050

51-
// BrowseCache provides caching for hierarchical path completion
51+
// BrowseCache provides caching for hierarchical path completion.
5252
type BrowseCache struct {
5353
cacheDir string
5454
entries map[string]*CacheEntry
5555
mu sync.RWMutex
5656
dirty bool // Track if cache needs saving
5757
}
5858

59-
// Global cache instance
59+
// Global cache instance.
6060
var browseCache *BrowseCache
6161

62-
// NewBrowseCache creates a new browse cache
62+
// NewBrowseCache creates a new browse cache.
6363
func NewBrowseCache() (*BrowseCache, error) {
6464
cacheDir, err := os.UserCacheDir()
6565
if err != nil {
@@ -69,7 +69,7 @@ func NewBrowseCache() (*BrowseCache, error) {
6969
cacheDir = filepath.Join(cacheDir, "kefw2")
7070

7171
// Create cache directory if it doesn't exist
72-
if err := os.MkdirAll(cacheDir, 0755); err != nil {
72+
if err := os.MkdirAll(cacheDir, 0750); err != nil {
7373
return nil, fmt.Errorf("failed to create cache directory: %w", err)
7474
}
7575

@@ -84,7 +84,7 @@ func NewBrowseCache() (*BrowseCache, error) {
8484
return cache, nil
8585
}
8686

87-
// InitCache initializes the global browse cache
87+
// InitCache initializes the global browse cache.
8888
func InitCache() {
8989
var err error
9090
browseCache, err = NewBrowseCache()
@@ -97,18 +97,18 @@ func InitCache() {
9797
}
9898
}
9999

100-
// cacheFilePath returns the path to the cache file
100+
// cacheFilePath returns the path to the cache file.
101101
func (c *BrowseCache) cacheFilePath() string {
102102
return filepath.Join(c.cacheDir, "browse_cache.json")
103103
}
104104

105-
// IsEnabled returns whether caching is enabled
105+
// IsEnabled returns whether caching is enabled.
106106
func (c *BrowseCache) IsEnabled() bool {
107107
return viper.GetBool("cache.enabled")
108108
}
109109

110-
// GetTTL returns the TTL for a given service type
111-
// Defaults are set in root.go initConfig(): default=300, radio=300, podcast=300, upnp=60
110+
// GetTTL returns the TTL for a given service type.
111+
// Defaults are set in root.go initConfig(): default=300, radio=300, podcast=300, upnp=60.
112112
func (c *BrowseCache) GetTTL(service string) time.Duration {
113113
key := fmt.Sprintf("cache.ttl_%s", service)
114114
seconds := viper.GetInt(key)
@@ -122,7 +122,7 @@ func (c *BrowseCache) GetTTL(service string) time.Duration {
122122
return time.Duration(seconds) * time.Second
123123
}
124124

125-
// Get retrieves items from cache if valid
125+
// Get retrieves items from cache if valid.
126126
func (c *BrowseCache) Get(path string, service string) ([]CachedItem, bool) {
127127
if !c.IsEnabled() {
128128
return nil, false
@@ -146,7 +146,7 @@ func (c *BrowseCache) Get(path string, service string) ([]CachedItem, bool) {
146146
return entry.Items, true
147147
}
148148

149-
// Set stores items in cache
149+
// Set stores items in cache.
150150
func (c *BrowseCache) Set(path string, service string, items []CachedItem) {
151151
if !c.IsEnabled() {
152152
return
@@ -163,7 +163,7 @@ func (c *BrowseCache) Set(path string, service string, items []CachedItem) {
163163
c.dirty = true
164164
}
165165

166-
// Clear removes all cached data
166+
// Clear removes all cached data.
167167
func (c *BrowseCache) Clear() error {
168168
c.mu.Lock()
169169
defer c.mu.Unlock()
@@ -175,7 +175,7 @@ func (c *BrowseCache) Clear() error {
175175
return os.Remove(c.cacheFilePath())
176176
}
177177

178-
// Status returns cache statistics
178+
// Status returns cache statistics.
179179
func (c *BrowseCache) Status() (entries int, size int64, oldestAge time.Duration) {
180180
c.mu.RLock()
181181
defer c.mu.RUnlock()
@@ -201,7 +201,7 @@ func (c *BrowseCache) Status() (entries int, size int64, oldestAge time.Duration
201201
return
202202
}
203203

204-
// Load loads cache from disk
204+
// Load loads cache from disk.
205205
func (c *BrowseCache) Load() error {
206206
c.mu.Lock()
207207
defer c.mu.Unlock()
@@ -223,7 +223,7 @@ func (c *BrowseCache) Load() error {
223223
return nil
224224
}
225225

226-
// Save persists cache to disk
226+
// Save persists cache to disk.
227227
func (c *BrowseCache) Save() error {
228228
c.mu.Lock()
229229
defer c.mu.Unlock()
@@ -246,20 +246,20 @@ func (c *BrowseCache) Save() error {
246246
return err
247247
}
248248

249-
if err := os.WriteFile(c.cacheFilePath(), data, 0644); err != nil {
249+
if err := os.WriteFile(c.cacheFilePath(), data, 0600); err != nil {
250250
return err
251251
}
252252

253253
c.dirty = false
254254
return nil
255255
}
256256

257-
// CacheDir returns the cache directory path
257+
// CacheDir returns the cache directory path.
258258
func (c *BrowseCache) CacheDir() string {
259259
return c.cacheDir
260260
}
261261

262-
// FindItemByTitle finds a cached item by its title within a parent path
262+
// FindItemByTitle finds a cached item by its title within a parent path.
263263
func (c *BrowseCache) FindItemByTitle(parentPath, service, title string) (*CachedItem, bool) {
264264
items, ok := c.Get(parentPath, service)
265265
if !ok {
@@ -274,8 +274,8 @@ func (c *BrowseCache) FindItemByTitle(parentPath, service, title string) (*Cache
274274
return nil, false
275275
}
276276

277-
// ResolveDisplayPath resolves a display path (title-based) to an API path
278-
// Returns the API path and the last item, or empty string if not found
277+
// ResolveDisplayPath resolves a display path (title-based) to an API path.
278+
// Returns the API path and the last item, or empty string if not found.
279279
func (c *BrowseCache) ResolveDisplayPath(displayPath, service string) (string, *CachedItem, bool) {
280280
if displayPath == "" {
281281
return "", nil, true // Empty path = top level, no API path needed
@@ -318,7 +318,7 @@ func (c *BrowseCache) ResolveDisplayPath(displayPath, service string) (string, *
318318
// Cache Command
319319
// ============================================
320320

321-
// cacheCmd represents the cache command
321+
// cacheCmd represents the cache command.
322322
var cacheCmd = &cobra.Command{
323323
Use: "cache",
324324
Short: "Manage browse cache for tab completion",
@@ -328,11 +328,11 @@ The cache stores browse results to speed up tab completion.
328328
Cache location: ~/.cache/kefw2/`,
329329
}
330330

331-
// cacheClearCmd clears the cache
331+
// cacheClearCmd clears the cache.
332332
var cacheClearCmd = &cobra.Command{
333333
Use: "clear",
334334
Short: "Clear all cached browse data",
335-
Run: func(cmd *cobra.Command, args []string) {
335+
Run: func(_ *cobra.Command, _ []string) {
336336
if browseCache == nil {
337337
errorPrinter.Println("Cache not initialized")
338338
return
@@ -345,11 +345,11 @@ var cacheClearCmd = &cobra.Command{
345345
},
346346
}
347347

348-
// cacheStatusCmd shows cache status
348+
// cacheStatusCmd shows cache status.
349349
var cacheStatusCmd = &cobra.Command{
350350
Use: "status",
351351
Short: "Show cache statistics",
352-
Run: func(cmd *cobra.Command, args []string) {
352+
Run: func(_ *cobra.Command, _ []string) {
353353
// Get cache directory
354354
cacheDir, err := os.UserCacheDir()
355355
if err != nil {
@@ -361,7 +361,7 @@ var cacheStatusCmd = &cobra.Command{
361361
rowsCachePath := filepath.Join(cacheDir, "rows_cache.json")
362362
if info, err := os.Stat(rowsCachePath); err == nil {
363363
// Read and parse the cache file to count entries
364-
data, readErr := os.ReadFile(rowsCachePath)
364+
data, readErr := os.ReadFile(rowsCachePath) //nolint:gosec // Path is constructed from user's cache dir
365365
var entryCount int
366366
var oldestAge time.Duration
367367
if readErr == nil {
@@ -422,7 +422,7 @@ var cacheStatusCmd = &cobra.Command{
422422
},
423423
}
424424

425-
// formatBytes formats bytes as human-readable string
425+
// formatBytes formats bytes as human-readable string.
426426
func formatBytes(bytes int64) string {
427427
const unit = 1024
428428
if bytes < unit {

0 commit comments

Comments
 (0)