Skip to content

Commit be6ad1a

Browse files
authored
feat(ui): display album art in player footer (#63)
1 parent 829ff39 commit be6ad1a

File tree

10 files changed

+170
-103
lines changed

10 files changed

+170
-103
lines changed

go.mod

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
11
module github.com/MattiaPun/SubTUI/v2
22

3-
go 1.25.4
3+
go 1.26
44

55
require (
66
github.com/atotto/clipboard v0.1.4
77
github.com/babycommando/rich-go v0.0.0-20250721232012-541cbf3cf142
88
github.com/charmbracelet/bubbles v0.21.0
99
github.com/charmbracelet/bubbletea v1.3.10
1010
github.com/charmbracelet/lipgloss v1.1.0
11+
github.com/charmbracelet/x/mosaic v0.0.0-20260305213658-fe36e8c10185
1112
github.com/gdrens/mpv v0.0.0-20220831113119-9a418870d1b5
1213
github.com/gen2brain/beeep v0.11.2
1314
github.com/godbus/dbus/v5 v5.2.2
15+
github.com/lrstanley/bubblezone v1.0.0
1416
github.com/mattn/go-runewidth v0.0.19
1517
github.com/pelletier/go-toml/v2 v2.2.4
1618
github.com/rmhubbert/bubbletea-overlay v0.6.3
19+
golang.org/x/text v0.34.0
1720
)
1821

1922
require (
2023
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
2124
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
22-
github.com/charmbracelet/colorprofile v0.3.3 // indirect
23-
github.com/charmbracelet/x/ansi v0.11.3 // indirect
24-
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
25+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
26+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
27+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
2528
github.com/charmbracelet/x/term v0.2.2 // indirect
26-
github.com/clipperhouse/displaywidth v0.6.1 // indirect
29+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
2730
github.com/clipperhouse/stringish v0.1.1 // indirect
28-
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
31+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
2932
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
3033
github.com/esiqveland/notify v0.13.3 // indirect
3134
github.com/go-ole/go-ole v1.3.0 // indirect
3235
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
33-
github.com/lrstanley/bubblezone v1.0.0 // indirect
3436
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
3537
github.com/mattn/go-isatty v0.0.20 // indirect
3638
github.com/mattn/go-localereader v0.0.1 // indirect
@@ -43,7 +45,7 @@ require (
4345
github.com/sergeymakinen/go-ico v1.0.0 // indirect
4446
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
4547
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
46-
golang.org/x/sys v0.39.0 // indirect
47-
golang.org/x/text v0.31.0 // indirect
48+
golang.org/x/image v0.36.0 // indirect
49+
golang.org/x/sys v0.41.0 // indirect
4850
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
4951
)

go.sum

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,24 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
1010
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
1111
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
1212
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
13-
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
14-
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
13+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
14+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
1515
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
1616
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
17-
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
18-
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
19-
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
20-
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
17+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
18+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
19+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
20+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
21+
github.com/charmbracelet/x/mosaic v0.0.0-20260305213658-fe36e8c10185 h1:as6jMHV4OdewfNQ6NUuJ13dQeaM35LMefthJhGQFEDo=
22+
github.com/charmbracelet/x/mosaic v0.0.0-20260305213658-fe36e8c10185/go.mod h1:KiC0LDz54wnn4PcCtoUujxia6NjFyuD3BgXpbqh0EGU=
2123
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
2224
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
23-
github.com/clipperhouse/displaywidth v0.6.1 h1:/zMlAezfDzT2xy6acHBzwIfyu2ic0hgkT83UX5EY2gY=
24-
github.com/clipperhouse/displaywidth v0.6.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
25+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
26+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
2527
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
2628
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
27-
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
28-
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
29+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
30+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
2931
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3032
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3133
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -88,13 +90,15 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
8890
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
8991
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
9092
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
93+
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
94+
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
9195
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9296
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9397
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
94-
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
95-
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
96-
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
97-
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
98+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
99+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
100+
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
101+
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
98102
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
99103
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
100104
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=

internal/api/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ func SubsonicCoverArtUrl(id string, size int) string {
326326
return url
327327
}
328328

329-
func SubsonicCoverArt(id string) ([]byte, error) {
330-
url := SubsonicCoverArtUrl(id, 50)
329+
func SubsonicCoverArt(id string, size int) ([]byte, error) {
330+
url := SubsonicCoverArtUrl(id, size)
331331
resp, err := httpClient.Get(url)
332332
if err != nil {
333333
return nil, err

internal/api/config.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ type App struct {
5050
}
5151

5252
type Theme struct {
53-
Subtle []string `toml:"subtle"`
54-
Highlight []string `toml:"highlight"`
55-
Special []string `toml:"special"`
56-
Filtered []string `toml:"filtered"`
53+
DisplayAlbumArt bool `toml:"display_album_art"`
54+
Subtle []string `toml:"subtle"`
55+
Highlight []string `toml:"highlight"`
56+
Special []string `toml:"special"`
57+
Filtered []string `toml:"filtered"`
5758
}
5859

5960
type Filters struct {

internal/api/config.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ discord_rich_presence = true
55
mouse_support = false
66

77
[theme]
8+
display_album_art = true
89
# Format: ['Light Color', 'Dark Color']
9-
subtle = ['#D9DCCF', '#6B6B6BFF']
10-
highlight = ['#874BFD', '#7D56F4']
11-
special = ['#43BF6D', '#73F59F']
12-
filtered = ['#A9A9A9', '#555555']
10+
subtle = ['#D9DCCF', '#6B6B6BFF']
11+
highlight = ['#874BFD', '#7D56F4']
12+
special = ['#43BF6D', '#73F59F']
13+
filtered = ['#A9A9A9', '#555555']
1314

1415
[filters]
1516
titles = [] # Exclude songs with titles containing these strings

internal/ui/cmds_api.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package ui
22

33
import (
4+
"bytes"
5+
"image/jpeg"
6+
47
"github.com/MattiaPun/SubTUI/v2/internal/api"
58
tea "github.com/charmbracelet/bubbletea"
69
)
@@ -138,6 +141,22 @@ func toggleStarCmd(id string, isCurrentlyStarred bool) tea.Cmd {
138141
}
139142
}
140143

144+
func getCoverArtCmd(songID string) tea.Cmd {
145+
return func() tea.Msg {
146+
imgData, err := api.SubsonicCoverArt(songID, 500)
147+
if err != nil {
148+
return nil
149+
}
150+
151+
img, err := jpeg.Decode(bytes.NewReader(imgData))
152+
if err != nil {
153+
return nil
154+
}
155+
156+
return coverArtMsg{img: img}
157+
}
158+
}
159+
141160
func getPlayQueue() tea.Cmd {
142161
return func() tea.Msg {
143162
result, err := api.SubsonicGetQueue()

internal/ui/model.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package ui
22

33
import (
4+
"image"
45
"time"
56

67
"github.com/MattiaPun/SubTUI/v2/internal/api"
78
"github.com/MattiaPun/SubTUI/v2/internal/integration"
89
"github.com/MattiaPun/SubTUI/v2/internal/player"
910
"github.com/charmbracelet/bubbles/textinput"
11+
"github.com/charmbracelet/x/mosaic"
1012
)
1113

1214
var albumTypes = []string{"All", "Random", "Favorites", "Recently Added", "Recently Played", "Most Played"}
@@ -38,6 +40,10 @@ type model struct {
3840
displayMode int
3941
displayModePrev int
4042

43+
// Cover Art
44+
coverArt image.Image
45+
coverMosaic mosaic.Mosaic
46+
4147
// App State
4248
err error
4349
loading bool
@@ -131,6 +137,10 @@ type playQueueResultMsg struct {
131137

132138
type viewStarredSongsMsg *api.SearchResult3
133139

140+
type coverArtMsg struct {
141+
img image.Image
142+
}
143+
134144
type createShareMsg struct {
135145
url string
136146
}

internal/ui/update.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4646
case viewStarredSongsMsg:
4747
return m.handleViewStarredSongs(msg)
4848

49+
case coverArtMsg:
50+
return m.handleCoverArt(msg)
51+
4952
case shuffledSongsMsg:
5053
return m.handleShuffledSongs(msg)
5154

internal/ui/update_messages.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/MattiaPun/SubTUI/v2/internal/player"
1414
"github.com/atotto/clipboard"
1515
tea "github.com/charmbracelet/bubbletea"
16+
"github.com/charmbracelet/x/mosaic"
1617
"github.com/gen2brain/beeep"
1718
zone "github.com/lrstanley/bubblezone"
1819
)
@@ -234,7 +235,7 @@ func (m model) handleStatus(msg statusMsg) (tea.Model, tea.Cmd) {
234235
// System notification
235236
if m.notify {
236237
go func() {
237-
artBytes, err := api.SubsonicCoverArt(currentSong.ID)
238+
artBytes, err := api.SubsonicCoverArt(currentSong.ID, 50)
238239

239240
title := "SubTUI"
240241
description := fmt.Sprintf("Playing %s - %s", currentSong.Title, currentSong.Artist)
@@ -257,6 +258,11 @@ func (m model) handleStatus(msg statusMsg) (tea.Model, tea.Cmd) {
257258
m.discordInstance.UpdateActivity(metadata)
258259
}
259260

261+
// Album Art Update
262+
if api.AppConfig.Theme.DisplayAlbumArt {
263+
cmds = append(cmds, getCoverArtCmd(currentSong.ID))
264+
}
265+
260266
windowTitle := fmt.Sprintf("%s - %s", metadata.Title, metadata.Artist)
261267
cmds = append(cmds, tea.SetWindowTitle(windowTitle))
262268
}
@@ -394,6 +400,12 @@ func (m model) handleViewStarredSongs(msg viewStarredSongsMsg) (tea.Model, tea.C
394400
return m, nil
395401
}
396402

403+
func (m model) handleCoverArt(msg coverArtMsg) (tea.Model, tea.Cmd) {
404+
m.coverArt = msg.img
405+
m.coverMosaic = mosaic.New().Width(16).Height(8)
406+
return m, nil
407+
}
408+
397409
func (m model) handleShuffledSongs(msg shuffledSongsMsg) (tea.Model, tea.Cmd) {
398410
if msg.updateView {
399411
m.songs = msg.songs

0 commit comments

Comments
 (0)