Skip to content

Commit 63b1e69

Browse files
tdiminoclaudebjarneo
authored
Album separators in playlist views (#190)
* Album separators in playlist views Tracks with album metadata now display grouped under dimmed separator headers in both the main playlist and the playlist manager. Scroll logic accounts for separator rows consuming budget—cursor stays visible across album boundaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Suppress album separators for Spotify playlist tracks Spotify playlists typically have every track from a different album, causing a separator on every row. Gate separator rendering behind an isStreamingPlaylistTrack check so library-style providers (local, Navidrome, Jellyfin, Plex) keep separators while Spotify skips them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Dedupe album-separator row counting Extract albumSeparatorRows as the single source of truth for both playlistScroll (scroll.go) and renderPlMgrTracks (view_overlays.go). The playlist manager's inline scroll-row counter was missing the isStreamingPlaylistTrack exclusion that its renderer applied, so mixed Spotify+local playlists could mis-scroll. Routing both paths through one helper fixes the inconsistency. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Bjarne Øverli <bjarne.oeverli@gmail.com>
1 parent c5a9da9 commit 63b1e69

7 files changed

Lines changed: 80 additions & 13 deletions

File tree

docs/playlists.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ Press `p` from any view to open the playlist manager:
141141
6. **Play all**: press `Enter` on the track list to load all tracks into the player
142142
7. **New playlist**: select "+ New Playlist...", type a name, and press Enter
143143

144+
Tracks with an `album` field are grouped by album with visual separator headers in both the playlist manager and the main player view.
145+
144146
The directory `~/.config/cliamp/playlists/` is created automatically on first use. Removing the last track from a playlist auto-deletes the file.
145147

146148
### Creating Playlists Manually

site/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1522,7 +1522,7 @@
15221522
<div class="feature">
15231523
<div class="feature-icon"></div>
15241524
<div class="feature-name">Playlists</div>
1525-
<p>TOML playlists, M3U/M3U8/PLS support, built-in playlist manager.</p>
1525+
<p>TOML playlists, M3U/M3U8/PLS support, playlist manager with album grouping.</p>
15261526
</div>
15271527
<div class="feature">
15281528
<div class="feature-icon"></div>

ui/model/scroll.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ func (m Model) playlistScroll(visible int) int {
6666
if m.plCursor < scroll {
6767
return m.plCursor
6868
}
69-
if m.plCursor-scroll+1 <= visible {
70-
return scroll
69+
for scroll < m.plCursor && albumSeparatorRows(tracks, scroll, m.plCursor) > visible {
70+
scroll++
7171
}
72-
return m.plCursor - visible + 1
72+
return scroll
7373
}
7474

7575
func (m Model) mainFrameFixedLines(includeTransient bool) int {

ui/model/view.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -623,12 +623,24 @@ func (m Model) renderPlaylist() string {
623623
currentIdx := m.playlist.Index()
624624
scroll := m.playlistScroll(budget)
625625

626-
// budget is the number of rendered lines available for tracks.
627-
// The loop below counts every appended line against this budget
628-
// so the playlist never overflows its area.
629-
lines := make([]string, 0, budget) // tracks
626+
lines := make([]string, 0, budget)
630627
numWidth := len(fmt.Sprintf("%d", len(tracks)))
628+
prevAlbum := ""
629+
if scroll > 0 {
630+
prevAlbum = tracks[scroll-1].Album
631+
}
631632
for i := scroll; i < len(tracks) && len(lines) < budget; i++ {
633+
if album := tracks[i].Album; album != "" && album != prevAlbum && !isStreamingPlaylistTrack(tracks[i].Path) {
634+
if len(lines)+1 >= budget {
635+
break
636+
}
637+
lines = append(lines, m.albumSeparator(album, tracks[i].Year))
638+
}
639+
prevAlbum = tracks[i].Album
640+
if len(lines) >= budget {
641+
break
642+
}
643+
632644
prefix := " "
633645
style := playlistItemStyle
634646

ui/model/view_helpers.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"unicode/utf8"
77

8+
"cliamp/playlist"
89
"cliamp/ui"
910
)
1011

@@ -55,6 +56,36 @@ func helpKey(key, label string) string {
5556
return helpKeyStyle.Render(" "+key+" ") + helpStyle.Render(" "+label)
5657
}
5758

59+
// isStreamingPlaylistTrack reports whether path is a streaming-provider URI
60+
// whose Album metadata may not represent a real album grouping (so separators
61+
// would be misleading).
62+
func isStreamingPlaylistTrack(path string) bool {
63+
return strings.HasPrefix(path, "spotify:track:")
64+
}
65+
66+
// albumSeparatorRows counts rendered rows between scroll and cursor (inclusive)
67+
// in a playlist view that emits an album-separator row whenever the album
68+
// changes. Streaming tracks are treated as not contributing a separator,
69+
// matching the renderer.
70+
func albumSeparatorRows(tracks []playlist.Track, scroll, cursor int) int {
71+
if len(tracks) == 0 || scroll < 0 || cursor < scroll || cursor >= len(tracks) {
72+
return 0
73+
}
74+
rows := 0
75+
prevAlbum := ""
76+
if scroll > 0 {
77+
prevAlbum = tracks[scroll-1].Album
78+
}
79+
for i := scroll; i <= cursor; i++ {
80+
if album := tracks[i].Album; album != "" && album != prevAlbum && !isStreamingPlaylistTrack(tracks[i].Path) {
81+
rows++
82+
}
83+
prevAlbum = tracks[i].Album
84+
rows++
85+
}
86+
return rows
87+
}
88+
5889
// albumSeparator builds a full-width album divider line.
5990
func (m Model) albumSeparator(album string, year int) string {
6091
prefix := "── "

ui/model/view_nav.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (m Model) renderNavTrackList() []string {
197197
for i := scroll; i < len(m.navBrowser.tracks) && rendered < maxVisible; i++ {
198198
t := m.navBrowser.tracks[i]
199199

200-
if album := t.Album; album != "" && album != prevAlbum {
200+
if album := t.Album; album != "" && album != prevAlbum && !isStreamingPlaylistTrack(t.Path) {
201201
lines = append(lines, m.albumSeparator(album, t.Year))
202202
if rendered >= maxVisible {
203203
break

ui/model/view_overlays.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,38 @@ func (m Model) renderPlMgrTracks() []string {
155155
}
156156

157157
maxVisible := 12
158+
tracks := m.plManager.tracks
158159
scroll := scrollStart(m.plManager.cursor, maxVisible)
160+
for scroll < m.plManager.cursor && albumSeparatorRows(tracks, scroll, m.plManager.cursor) > maxVisible {
161+
scroll++
162+
}
159163

160-
for i := scroll; i < len(m.plManager.tracks) && i < scroll+maxVisible; i++ {
161-
name := truncate(m.plManager.tracks[i].DisplayName(), ui.PanelWidth-8)
164+
rendered := 0
165+
prevAlbum := ""
166+
if scroll > 0 {
167+
prevAlbum = tracks[scroll-1].Album
168+
}
169+
170+
for i := scroll; i < len(tracks) && rendered < maxVisible; i++ {
171+
if album := tracks[i].Album; album != "" && album != prevAlbum && !isStreamingPlaylistTrack(tracks[i].Path) {
172+
if rendered+1 >= maxVisible {
173+
break
174+
}
175+
lines = append(lines, m.albumSeparator(album, tracks[i].Year))
176+
rendered++
177+
}
178+
prevAlbum = tracks[i].Album
179+
if rendered >= maxVisible {
180+
break
181+
}
182+
name := truncate(tracks[i].DisplayName(), ui.PanelWidth-8)
162183
label := fmt.Sprintf("%d. %s", i+1, name)
163184
lines = append(lines, cursorLine(label, i == m.plManager.cursor))
185+
rendered++
164186
}
165187

166-
if len(m.plManager.tracks) > maxVisible {
167-
lines = append(lines, "", dimStyle.Render(fmt.Sprintf(" %d/%d tracks", m.plManager.cursor+1, len(m.plManager.tracks))))
188+
if len(tracks) > maxVisible {
189+
lines = append(lines, "", dimStyle.Render(fmt.Sprintf(" %d/%d tracks", m.plManager.cursor+1, len(tracks))))
168190
}
169191

170192
lines = append(lines, "", helpKey("↑↓", "Navigate ")+helpKey("Enter", "Play all ")+helpKey("a", "Add track ")+helpKey("d", "Remove ")+helpKey("Esc", "Back"))

0 commit comments

Comments
 (0)